要解决的问题
论文论断、代码仓库和实际产物可能互不一致。只读论文,或让所有审查者使用已经混合的上下文,容易隐藏缺失证据与矛盾。
并行审查不同证据来源,暴露分歧,并形成结构化的论文复现 go / no-go 计划。
适合需要判断论文是否值得、以及如何复现的研究团队
论文论断、代码仓库和实际产物可能互不一致。只读论文,或让所有审查者使用已经混合的上下文,容易隐藏缺失证据与矛盾。
默认使用明确标记的证据快照;live 模式可以接入 Asta 与 GitHub。
每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。
来源专属 Agent 并行检查不同证据快照。
提取论文论断与报告的实验设置。
检查可用实现与产物。
记录来源质量、缺口和冲突。
规划者通过依赖上下文接收结构化审查结果。
调和论文论断与产物,形成具体 go / no-go 计划。
计划明确展示不确定性和预置的来源冲突,不会给出虚假的整洁答案。
上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。
OpenMultiAgent 在仓库的克隆里运行这个文件:
npx tsx packages/core/examples/cookbook/paper-replication-triage.tsnpx tsx packages/core/examples/cookbook/paper-replication-triage.ts "paper title or arXiv id"
完整示例,从固定的 Framework commit 同步。
/*** Paper Replication Triage (Multi-Source Evidence Reconciliation)** Demonstrates:* - Source-specific agents audit different evidence snapshots, not one shared PDF* - runTasks() DAG: parallel source audits -> dependent replication planner* - Shared memory/dependency context carries upstream JSON into the planner* - Seeded conflicts force the planner to reconcile paper claims vs artifacts* - Mocked source snapshots are flagged; SOURCE_MODE=live uses Asta + GitHub** Run:* npx tsx packages/core/examples/cookbook/paper-replication-triage.ts* npx tsx packages/core/examples/cookbook/paper-replication-triage.ts "paper title or arXiv id"** Optional:* SOURCE_MODE=live npx tsx packages/core/examples/cookbook/paper-replication-triage.ts "ARXIV:1706.03762"** Prerequisites:* LLM_PROVIDER=anthropic (default) requires ANTHROPIC_API_KEY.* Other supported values: openai, gemini, groq, openrouter.* Gemini accepts GEMINI_API_KEY or GOOGLE_API_KEY.* SOURCE_MODE=live requires ASTA_API_KEY. GITHUB_TOKEN is optional but recommended.* The live Asta path uses the optional @modelcontextprotocol/sdk peer dependency.*/import { readFileSync } from 'node:fs'import { fileURLToPath } from 'node:url'import path from 'node:path'import { z } from 'zod'import { OpenMultiAgent } from '../../src/index.js'import type { AgentConfig, OrchestratorEvent, TaskExecutionRecord } from '../../src/types.js'const __dirname = path.dirname(fileURLToPath(import.meta.url))const FIXTURE_DIR = path.join(__dirname, '../fixtures/paper-replication-triage')const DEFAULT_QUERY = 'SparseCheck: Calibrated Retrieval for Low-Resource Scientific QA'const PAPER_QUERY = process.argv[2] ?? DEFAULT_QUERYconst REPO_EVIDENCE_LIMIT = 3const REPO_DISCOVERY_FETCH_LIMIT = 6const SUPPORT_FILE_EXCERPT_LIMIT = 450const README_EXCERPT_LIMIT = 1400const ISSUE_BODY_EXCERPT_LIMIT = 360const RELEVANT_PATH_LIMIT = 60const ISSUE_EVIDENCE_LIMIT = 6const DATASET_CANDIDATE_LIMIT_PER_REPO = 8const DATASET_CANDIDATE_MIN_CONFIDENCE = 0.65const DATASET_CONTEXT_LINE_LIMIT = 4const DATASET_RELATED_PATH_LIMIT = 12type SourceMode = 'mock' | 'live'type ProviderChoice = 'anthropic' | 'openai' | 'gemini' | 'groq' | 'openrouter'type SnippetSearchScope = 'paper_scoped' | 'global_unscoped'type PaperMetadataScope = 'target_scoped' | 'unresolved_unscoped'interface SourceBundle {mode: SourceModequery: stringsourceIndex: unknownscholarlyMetadata: unknowncodeArtifacts: unknowndatasetArtifacts: unknowncitationFeedback: unknownsourceDigest?: SourceDigest}interface SourceDigest {paper: {title: stringurl: stringvenue_or_year: string}repositories: Array<{repository: stringurl: stringdescription: stringrelationship_hints: string[]useful_paths: string[]issue_signals: Array<{title: stringurl: string}>}>datasets: Array<{name: stringevidence: stringsource: string}>dataset_snippet_mentions: string[]reproduction_signals: Array<{title: stringurl: stringsource: string}>}interface ProviderSettings {label: ProviderChoiceprovider: NonNullable<AgentConfig['provider']>model: stringbaseURL?: stringapiKey?: stringmissingEnv?: string}type DatasetEvidenceKind =| 'cli_arg'| 'data_path'| 'repository_path'| 'readme_line'| 'support_file_line'interface DatasetCandidateEvidence {name: stringevidence_kind: stringsource_path: stringevidence: stringconfidence: number}interface MCPClientLike {connect(transport: unknown, options?: { timeout?: number }): Promise<void>callTool(request: { name: string; arguments: Record<string, unknown> },resultSchema?: unknown,options?: { timeout?: number },): Promise<unknown>close?: () => Promise<void>}interface LivePaperIdentity {resolvedPaperId?: stringtargetPaperMetadata?: unknownmetadataScope: PaperMetadataScopenote: string}type MCPClientConstructor = new (info: { name: string; version: string },options: { capabilities: Record<string, unknown> },) => MCPClientLiketype StreamableHTTPClientTransportConstructor = new (url: URL,opts?: { requestInit?: RequestInit },) => unknown// ---------------------------------------------------------------------------// Provider selection// ---------------------------------------------------------------------------function resolveProvider(): ProviderSettings {const raw = (process.env.LLM_PROVIDER ?? 'anthropic').toLowerCase() as ProviderChoiceconst modelOverride = process.env.LLM_MODELswitch (raw) {case 'openai':return {label: 'openai',provider: 'openai',model: modelOverride ?? 'gpt-4o-mini',baseURL: process.env.OPENAI_BASE_URL,missingEnv: process.env.OPENAI_API_KEY ? undefined : 'OPENAI_API_KEY',}case 'gemini':return {label: 'gemini',provider: 'gemini',model: modelOverride ?? 'gemini-2.5-flash',missingEnv: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY? undefined: 'GEMINI_API_KEY or GOOGLE_API_KEY',}case 'groq':return {label: 'groq',provider: 'openai',baseURL: 'https://api.groq.com/openai/v1',apiKey: process.env.GROQ_API_KEY,model: modelOverride ?? 'llama-3.3-70b-versatile',missingEnv: process.env.GROQ_API_KEY ? undefined : 'GROQ_API_KEY',}case 'openrouter':return {label: 'openrouter',provider: 'openai',baseURL: 'https://openrouter.ai/api/v1',apiKey: process.env.OPENROUTER_API_KEY,model: modelOverride ?? 'openai/gpt-4o-mini',missingEnv: process.env.OPENROUTER_API_KEY ? undefined : 'OPENROUTER_API_KEY',}case 'anthropic':default:return {label: 'anthropic',provider: 'anthropic',model: modelOverride ?? 'claude-sonnet-4-6',missingEnv: process.env.ANTHROPIC_API_KEY ? undefined : 'ANTHROPIC_API_KEY',}}}const PROVIDER = resolveProvider()// ---------------------------------------------------------------------------// Source loading// ---------------------------------------------------------------------------function readJson<T = unknown>(name: string): T {const filePath = path.join(FIXTURE_DIR, name)return JSON.parse(readFileSync(filePath, 'utf-8')) as T}function resolveSourceMode(): SourceMode {const raw = (process.env.SOURCE_MODE ?? 'mock').toLowerCase()if (raw === 'live') return 'live'if (raw !== 'mock') {console.warn(`[warn] Unknown SOURCE_MODE=${raw}; falling back to mock snapshots.`)}return 'mock'}function loadMockSourceBundle(query: string): SourceBundle {return {mode: 'mock',query,sourceIndex: readJson('source-index.json'),scholarlyMetadata: readJson('scholarly-metadata.json'),codeArtifacts: readJson('code-artifacts.json'),datasetArtifacts: readJson('dataset-artifacts.json'),citationFeedback: readJson('citation-feedback.json'),}}async function fetchJson<T>(url: string, headers: Record<string, string> = {}): Promise<T | undefined> {try {const response = await fetch(url, { headers })if (!response.ok) {return undefined}return await response.json() as T} catch {return undefined}}async function fetchText(url: string, headers: Record<string, string> = {}): Promise<string | undefined> {try {const response = await fetch(url, { headers })if (!response.ok) {return undefined}return await response.text()} catch {return undefined}}function getString(record: Record<string, unknown>, key: string): string | undefined {const value = record[key]return typeof value === 'string' ? value : undefined}function getNumber(record: Record<string, unknown>, key: string): number | undefined {const value = record[key]return typeof value === 'number' ? value : undefined}function compactText(text: string, maxLength = 180): string {const compacted = text.replace(/\s+/g, ' ').trim()return compacted.length > maxLength ? `${compacted.slice(0, maxLength - 3)}...` : compacted}function uniqueStrings(values: string[], limit = values.length): string[] {const seen = new Set<string>()const result: string[] = []for (const value of values) {const normalized = value.trim()if (!normalized || seen.has(normalized.toLowerCase())) continueseen.add(normalized.toLowerCase())result.push(normalized)if (result.length >= limit) break}return result}function normalizeTitleForCompare(title: string): string {return title.toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim()}function compactSearchItem(item: Record<string, unknown>): Record<string, unknown> {return {full_name: getString(item, 'full_name'),html_url: getString(item, 'html_url'),description: getString(item, 'description'),stargazers_count: getNumber(item, 'stargazers_count'),pushed_at: getString(item, 'pushed_at'),default_branch: getString(item, 'default_branch'),}}function collectStrings(value: unknown, acc: string[] = []): string[] {if (typeof value === 'string') {acc.push(value)return acc}if (Array.isArray(value)) {for (const item of value) collectStrings(item, acc)return acc}if (value !== null && typeof value === 'object') {for (const child of Object.values(value as Record<string, unknown>)) {collectStrings(child, acc)}}return acc}function findFirstNumberByKey(value: unknown, keyName: string): number | undefined {if (value === null || typeof value !== 'object') return undefinedif (Array.isArray(value)) {for (const item of value) {const found = findFirstNumberByKey(item, keyName)if (found !== undefined) return found}return undefined}const record = value as Record<string, unknown>const direct = record[keyName]if (typeof direct === 'number') return directfor (const child of Object.values(record)) {const found = findFirstNumberByKey(child, keyName)if (found !== undefined) return found}return undefined}function findFirstStringByKey(value: unknown, keyName: string): string | undefined {if (value === null || typeof value !== 'object') return undefinedif (Array.isArray(value)) {for (const item of value) {const found = findFirstStringByKey(item, keyName)if (found) return found}return undefined}const record = value as Record<string, unknown>const direct = record[keyName]if (typeof direct === 'string' && direct.trim() !== '') return directfor (const child of Object.values(record)) {const found = findFirstStringByKey(child, keyName)if (found) return found}return undefined}function extractCandidateTerms(text: string): string[] {const stop = new Set(['the','and','for','with','from','that','this','paper','using','based','model','models','method','methods','learning',])const terms = new Set<string>()const hyphenated = text.match(/\b[A-Za-z][A-Za-z0-9]*(?:[-_][A-Za-z0-9]+)+\b/g) ?? []const acronyms = text.match(/\b[A-Z][A-Z0-9]{2,}\b/g) ?? []for (const term of [...hyphenated, ...acronyms]) {const normalized = term.trim()if (normalized.length < 3 || normalized.length > 40) continueif (stop.has(normalized.toLowerCase())) continueterms.add(normalized)}return Array.from(terms).slice(0, 6)}function extractGithubRepoFullNames(value: unknown): string[] {const text = collectStrings(value).join('\n')const matches = text.matchAll(/https?:\/\/github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)/g)const repos = new Set<string>()for (const match of matches) {repos.add(match[1].replace(/\.git$/i, ''))}return Array.from(repos).slice(0, 5)}function buildGitHubSearchQueries(query: string, targetPaperMetadata: unknown): string[] {const title = findFirstStringByKey(targetPaperMetadata, 'title')const text = collectStrings(targetPaperMetadata).join('\n')const queries = new Set<string>()for (const candidate of [title, query]) {const trimmed = candidate?.trim()if (!trimmed) continuequeries.add(`"${trimmed}" in:readme`)queries.add(`${trimmed} in:name,readme`)}for (const term of extractCandidateTerms(text)) {queries.add(`${term} in:name,readme`)}return Array.from(queries).slice(0, 8)}function textContainsNormalizedPhrase(text: string, phrase: string): boolean {const normalizedPhrase = normalizeTitleForCompare(phrase)if (normalizedPhrase.length < 3) return falsereturn normalizeTitleForCompare(text).includes(normalizedPhrase)}function textMentionsTarget(text: string, target: string): boolean {if (textContainsNormalizedPhrase(text, target)) return trueconst terms = extractCandidateTerms(target)return terms.some((term) => {const pattern = new RegExp(`(^|[^A-Za-z0-9])${escapeRegExp(term)}([^A-Za-z0-9]|$)`, 'i')return pattern.test(text)})}function textWindow(text: string, index: number, before = 240, after = 240): string {return text.slice(Math.max(0, index - before), Math.min(text.length, index + after))}function hasNegatedOfficialPrefix(text: string, index: number, matchLength: number): boolean {const local = text.slice(Math.max(0, index - 48), index + matchLength).toLowerCase()return /\b(?:un|non)[-\s]?official\b/.test(local)|| /\bnot\s+(?:an?\s+|the\s+)?official\b/.test(local)|| /\bno\s+official\b/.test(local)|| /\bwithout\s+(?:an?\s+|the\s+)?official\b/.test(local)}function nonOfficialClaimNearTarget(text: string, target: string | undefined): boolean {const nonOfficialPattern =/\b(?:(?:un|non)[-\s]?official|third[-\s]?party|community|independent)\s+(?:implementation|code|repo(?:sitory)?|release|reproduction)\b|\bnot\s+(?:an?\s+|the\s+)?official\s+(?:implementation|code|repo(?:sitory)?|release)\b/giconst matches = [...text.matchAll(nonOfficialPattern)]if (matches.length === 0) return falseif (!target) return truereturn matches.some((match) => {const index = match.index ?? 0return textMentionsTarget(textWindow(text, index), target)})}function officialClaimNearTarget(text: string, target: string | undefined): boolean {const officialPattern =/\bofficial(?:\s+(?:implementation|code|repo(?:sitory)?|github|release))?\b|\bauthors?['’]?\s+(?:implementation|code|repo(?:sitory)?|release)\b/giconst matches = [...text.matchAll(officialPattern)]if (matches.length === 0) return falsereturn matches.some((match) => {const index = match.index ?? 0const matchText = match[0] ?? ''if (hasNegatedOfficialPrefix(text, index, matchText.length)) return falseif (!target) return truereturn textMentionsTarget(textWindow(text, index), target)})}async function fetchGitHubSearch(rawQuery: string,headers: Record<string, string>,): Promise<Record<string, unknown> | undefined> {return fetchJson<Record<string, unknown>>(`https://api.github.com/search/repositories?q=${encodeURIComponent(rawQuery)}&per_page=5`,headers,)}function rawGitHubFileUrl(fullName: string, branch: string, filePath: string): string {const encodedBranch = branch.split('/').map(encodeURIComponent).join('/')const encodedPath = filePath.split('/').map(encodeURIComponent).join('/')return `https://raw.githubusercontent.com/${fullName}/${encodedBranch}/${encodedPath}`}function scoreSupportFilePath(filePath: string): number {if (/package-lock\.json|pnpm-lock\.yaml|yarn\.lock|\.png$|\.jpg$|\.jpeg$|\.gif$/i.test(filePath)) return 0let score = 0if (/README/i.test(filePath)) score += 4if (/(^|\/)scripts?\//i.test(filePath)) score += 5if (/(^|\/)(configs?|experiments?)\//i.test(filePath)) score += 4if (/(^|\/)(data|datasets?|download|prepare|preprocess)/i.test(filePath)) score += 4if (/(train|training|eval|evaluation|metrics?|run|launch)/i.test(filePath)) score += 3if (/\.(sh|py|yaml|yml|json|md)$/i.test(filePath)) score += 2return score}function selectSupportFilePaths(paths: string[]): string[] {return paths.filter((filePath) =>/README|scripts?|configs?|experiments?|data|datasets?|download|prepare|preprocess|train|training|eval|evaluation|metrics?|run|launch/i.test(filePath)|| /\.(sh|py|yaml|yml|json|md)$/i.test(filePath),).map((filePath) => ({ filePath, score: scoreSupportFilePath(filePath) })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath)).map((item) => item.filePath).slice(0, 4)}const DATASET_NAME_STOPWORDS = new Set(['a','an','and','arg','args','benchmark','benchmarks','benchmark.md','config','custom','data','dataset','datasets','data_path','default','download','driven','feature','features','file','input','input_size','label','loader','main','model','path','predict','predict_step','processed','readme','readme.md','root','root_path','script','target','task','task_id','term','then','test','the','train','training','valid','validation',])function normalizeDatasetCandidate(raw: string, evidenceKind: DatasetEvidenceKind): string | undefined {const fromPath = raw.split(/[\\/]/).filter(Boolean).at(-1) ?? rawconst cleaned = fromPath.replace(/^[-/\s]+/, '').replace(/\.(csv|tsv|txt|npy|npz|pkl|h5|json|md|py|sh|yaml|yml)$/i, '').replace(/["'`),\]]+$/g, '').replace(/^\[|\]$/g, '').trim()if (cleaned.length < 3 || cleaned.length > 60) return undefinedif (cleaned.includes('=')) return undefinedif (/^-/.test(cleaned)) return undefinedconst lower = cleaned.toLowerCase()if (DATASET_NAME_STOPWORDS.has(lower)) return undefinedif (/^(arg|args|cfg|config|param|kwargs?)[_.-]/i.test(cleaned)) return undefinedif (/(^|_)(path|size|len|length|step|id|type|flag|mode|root|features?)$/i.test(cleaned)) return undefinedif (/^\d+$/.test(cleaned)) return undefinedconst highPrecisionSource = evidenceKind === 'cli_arg'|| evidenceKind === 'data_path'|| evidenceKind === 'repository_path'const hasDigit = /\d/.test(cleaned)const isAcronym = /^[A-Z][A-Z0-9-]{2,}$/.test(cleaned)const isMixedCaseIdentifier = /^[A-Z][A-Za-z]+[A-Z0-9][A-Za-z0-9-]*$/.test(cleaned)const isCompoundDatasetLike = /^[A-Z][A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)+$/.test(cleaned)const isTitleCaseWord = /^[A-Z][a-z][A-Za-z0-9-]{2,}$/.test(cleaned)const isPlainLowercaseWord = /^[a-z][a-z0-9-]{2,}$/.test(cleaned)const looksLikeMethodOrVenue = /(?:benchmark|baseline|linear|transformer|former|forecast|lookback|window|aaai|neurips|icml|iclr|acl|cvpr|emnlp)$/i.test(cleaned)|| /(?:benchmark|linear|transformer|former|forecasting|lookback|window)/i.test(cleaned)const isAmbiguousShortAcronym = isAcronym && cleaned.length <= 3 && !highPrecisionSourceconst isLongAcronym = isAcronym && cleaned.length > 3if (isAmbiguousShortAcronym) return undefinedif (looksLikeMethodOrVenue && !['cli_arg', 'data_path'].includes(evidenceKind)) return undefinedif (!hasDigit&& !isLongAcronym&& !isMixedCaseIdentifier&& !isCompoundDatasetLike&& !isTitleCaseWord&& !(highPrecisionSource && isPlainLowercaseWord)) {return undefined}return cleaned}function escapeRegExp(value: string): string {return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}function datasetEvidenceBaseConfidence(evidenceKind: DatasetEvidenceKind): number {switch (evidenceKind) {case 'cli_arg':return 0.72case 'data_path':return 0.68case 'repository_path':return 0.58case 'support_file_line':return 0.54case 'readme_line':return 0.54}}function datasetEvidenceConfidence(name: string,evidenceKind: DatasetEvidenceKind,evidence: string,): number {let confidence = datasetEvidenceBaseConfidence(evidenceKind)if (new RegExp(`(^|[^A-Za-z0-9])${escapeRegExp(name)}\\.(csv|tsv|txt|npy|npz|pkl|h5|json)([^A-Za-z0-9]|$)`, 'i').test(evidence)) {confidence += 0.12}if (/dataset|benchmark|download|split|train|test|validation/i.test(evidence)) {confidence += 0.08}if (/--data(?:set)?|--data_path|data_path|root_path/i.test(evidence)) {confidence += 0.08}return Math.min(0.95, Number(confidence.toFixed(2)))}function makeDatasetCandidateEvidence(rawName: string,evidenceKind: DatasetEvidenceKind,sourcePath: string,evidence: string,): DatasetCandidateEvidence | undefined {const name = normalizeDatasetCandidate(rawName, evidenceKind)if (!name) return undefinedreturn {name,evidence_kind: evidenceKind,source_path: sourcePath,evidence: compactText(evidence, 220),confidence: datasetEvidenceConfidence(name, evidenceKind, evidence),}}function extractDatasetCandidateEvidenceFromText(text: string,sourcePath: string,defaultEvidenceKind: DatasetEvidenceKind,): DatasetCandidateEvidence[] {const candidates: DatasetCandidateEvidence[] = []const patterns: Array<{ kind: DatasetEvidenceKind, regex: RegExp }> = [{ kind: 'cli_arg', regex: /--data(?:set)?(?:\s+|=)["']?([A-Za-z0-9_.-]{3,60})/gi },{ kind: 'data_path', regex: /--data_path(?:\s+|=)["']?([A-Za-z0-9_./-]+\.(?:csv|tsv|txt|npy|npz|pkl|h5|json))/gi },{ kind: 'data_path', regex: /\b(?:data_path|filename|file_name|target_file)\s*[:=]\s*["']?([A-Za-z0-9_./-]+\.(?:csv|tsv|txt|npy|npz|pkl|h5|json))/gi },{ kind: defaultEvidenceKind, regex: /\b(?:dataset|data source|data file|benchmark dataset|dataset name)\b[^\n:=]{0,24}[:=]\s*["'`]?([A-Za-z0-9_.-]{3,60})/gi },{ kind: 'repository_path', regex: /(?:^|\/)(?:data|datasets?)\/([A-Za-z0-9_.-]{3,60})(?:\/|\.|$)/gi },]for (const { kind, regex } of patterns) {for (const match of text.matchAll(regex)) {const raw = match[1]if (!raw) continueconst candidate = makeDatasetCandidateEvidence(raw, kind, sourcePath, match[0])if (candidate) candidates.push(candidate)}}return candidates}function extractDatasetCandidateEvidenceFromPaths(paths: string[]): DatasetCandidateEvidence[] {const candidates: DatasetCandidateEvidence[] = []for (const filePath of paths) {const pathParts = filePath.split('/').filter(Boolean)const datasetDirIndex = pathParts.findIndex((part) => /^datasets?$/i.test(part))if (datasetDirIndex >= 0 && pathParts[datasetDirIndex + 1]) {const candidate = makeDatasetCandidateEvidence(pathParts[datasetDirIndex + 1], 'repository_path', filePath, filePath)if (candidate) candidates.push(candidate)}const basename = pathParts.at(-1) ?? filePathif (/\.(csv|tsv|txt|npy|npz|pkl|h5|json)$/i.test(basename) && /(^|\/)(data|datasets?|benchmark)/i.test(filePath)) {const candidate = makeDatasetCandidateEvidence(basename, 'data_path', filePath, filePath)if (candidate) candidates.push(candidate)}if (/(^|\/)(scripts?|experiments?)\//i.test(filePath) && /\.(sh|py|yaml|yml|json)$/i.test(basename)) {const candidate = makeDatasetCandidateEvidence(basename, 'repository_path', filePath, filePath)if (candidate) candidates.push(candidate)}}return candidates}function summarizeDatasetCandidates(evidenceItems: DatasetCandidateEvidence[],limit = DATASET_CANDIDATE_LIMIT_PER_REPO,): DatasetCandidateEvidence[] {const byName = new Map<string, DatasetCandidateEvidence & { evidence_count: number }>()for (const item of evidenceItems) {const key = item.name.toLowerCase()const current = byName.get(key)if (!current) {byName.set(key, { ...item, evidence_count: 1 })continue}const sourceKinds = new Set(current.evidence_kind.split('+'))sourceKinds.add(item.evidence_kind)current.evidence_kind = Array.from(sourceKinds).join('+')current.evidence_count += 1current.confidence = Math.min(0.98,Number((Math.max(current.confidence, item.confidence) + Math.min(0.2, current.evidence_count * 0.04)).toFixed(2)),)if (!current.evidence.includes(item.evidence)) {current.evidence = compactText(`${current.evidence} | ${item.evidence}`, 260)}if (!current.source_path.includes(item.source_path)) {current.source_path = compactText(`${current.source_path}; ${item.source_path}`, 180)}}return Array.from(byName.values()).filter((item) =>item.confidence >= DATASET_CANDIDATE_MIN_CONFIDENCE|| (item.evidence_count >= 2 && item.confidence >= 0.58),).sort((a, b) => b.confidence - a.confidence || a.name.localeCompare(b.name)).map(({ evidence_count: _evidenceCount, ...item }) => item).slice(0, limit)}function extractDatasetMentionsFromAstaSnippets(value: unknown): string[] {const mentions: string[] = []const text = collectStrings(value).filter((item) => /dataset|benchmark|forecast|time[- ]series|long[- ]term|experiment|result/i.test(item)).join('\n')const contextPatterns = [/\b(?:on|across|over|using|with|from)\s+([A-Za-z][A-Za-z0-9_.-]{2,60}(?:\s*(?:,|and|\/)\s*[A-Za-z][A-Za-z0-9_.-]{2,60}){0,6})/gi,/\b(?:datasets?|benchmarks?)\s*(?:include|includes|including|are|:)\s+([A-Za-z][A-Za-z0-9_.-]{2,60}(?:\s*(?:,|and|\/)\s*[A-Za-z][A-Za-z0-9_.-]{2,60}){0,10})/gi,]for (const pattern of contextPatterns) {for (const match of text.matchAll(pattern)) {const list = match[1] ?? ''for (const rawName of list.split(/\s*(?:,|and|\/)\s*/)) {const candidate = normalizeDatasetCandidate(rawName, 'readme_line')if (candidate) mentions.push(candidate)}}}return uniqueStrings(mentions, 12)}async function fetchGitHubRepoEvidence(input: Record<string, unknown> | string,headers: Record<string, string>,sourceQuery?: string,): Promise<Record<string, unknown> | undefined> {const fullName = typeof input === 'string' ? input : getString(input, 'full_name')if (!fullName) return undefinedconst apiBase = `https://api.github.com/repos/${fullName}`const issueQueries = ['reproduce', 'reproduction', 'result', 'dataset', 'metric']const repo = typeof input === 'string'? await fetchJson<Record<string, unknown>>(apiBase, headers): inputconst repoRecord = repo ?? (typeof input === 'string' ? { full_name: input } : input)const defaultBranch = getString(repoRecord, 'default_branch') ?? 'main'const [readme, tree, issueSearches] = await Promise.all([fetchText(`${apiBase}/readme`, {...headers,Accept: 'application/vnd.github.raw',}),fetchJson<Record<string, unknown>>(`${apiBase}/git/trees/${encodeURIComponent(defaultBranch)}?recursive=1`,headers,),Promise.all(issueQueries.map((term) =>fetchJson<Record<string, unknown>>('https://api.github.com/search/issues?q=' +encodeURIComponent(`repo:${fullName} ${term} in:title,body type:issue`) +'&per_page=2',headers,),),),])const treeItems = Array.isArray(tree?.['tree'])? tree['tree'] as Array<Record<string, unknown>>: []const treeTruncated = tree?.['truncated'] === trueconst paths = treeItems.map((entry) => getString(entry, 'path')).filter((pathValue): pathValue is string => pathValue !== undefined)const relevantPaths = paths.filter((pathValue) =>/(^|\/)(scripts?|experiments?|configs?|data|datasets?|train|training|eval|evaluation|README|requirements|environment|Dockerfile)/i.test(pathValue)|| /\.(sh|py|yaml|yml|json)$/i.test(pathValue),).slice(0, RELEVANT_PATH_LIMIT)const supportFilePaths = selectSupportFilePaths(paths)const supportFileExcerpts = await Promise.all(supportFilePaths.map(async (filePath) => {const url = rawGitHubFileUrl(fullName, defaultBranch, filePath)const text = await fetchText(url, headers)return {path: filePath,raw_url: url,excerpt: text ? compactText(text, SUPPORT_FILE_EXCERPT_LIMIT) : 'unavailable',}}),)const issueByUrl = new Map<string, Record<string, unknown>>()for (const issueSearch of issueSearches) {if (!Array.isArray(issueSearch?.['items'])) continuefor (const issue of issueSearch['items'] as Array<Record<string, unknown>>) {const url = getString(issue, 'html_url')if (url) issueByUrl.set(url, issue)}}const issues = Array.from(issueByUrl.values()).slice(0, ISSUE_EVIDENCE_LIMIT).map((issue) => ({title: getString(issue, 'title'),html_url: getString(issue, 'html_url'),state: getString(issue, 'state'),created_at: getString(issue, 'created_at'),body_excerpt: getString(issue, 'body')?.slice(0, ISSUE_BODY_EXCERPT_LIMIT),}))const readmeText = readme ?? ''const repoIdentityText = [fullName,getString(repoRecord, 'description') ?? '',readmeText,].join('\n')const linkedFromPaperMetadata = sourceQuery === 'github-url-from-paper-metadata'const targetQuery = linkedFromPaperMetadata ? undefined : sourceQueryconst hasOfficialClaim = officialClaimNearTarget(readmeText, undefined)const officialForTarget = targetQuery ? officialClaimNearTarget(readmeText, targetQuery) : hasOfficialClaimconst nonOfficialForTarget = targetQuery? nonOfficialClaimNearTarget(readmeText, targetQuery): nonOfficialClaimNearTarget(readmeText, undefined)const repoRelationshipHints = {linked_from_paper_metadata: linkedFromPaperMetadata,mentions_query_or_title: targetQuery ? textMentionsTarget(repoIdentityText, targetQuery) : false,mentions_official: officialForTarget,official_claim_context_unclear: hasOfficialClaim && !officialForTarget,explicit_non_official_claim: nonOfficialForTarget,mentions_reproduction: /reproduc|replicat|results?|benchmark/i.test(readmeText),mentions_datasets: /dataset|data preparation|download data|benchmark data/i.test(readmeText),}const datasetCandidateEvidence = summarizeDatasetCandidates([...extractDatasetCandidateEvidenceFromText(readmeText, 'README.md', 'readme_line'),...extractDatasetCandidateEvidenceFromPaths(relevantPaths),...supportFileExcerpts.flatMap((item) =>extractDatasetCandidateEvidenceFromText(item.excerpt, item.path, 'support_file_line'),),],)const datasetMentions = datasetCandidateEvidence.map((candidate) => candidate.name)return {...compactSearchItem(repoRecord),html_url: getString(repoRecord, 'html_url') ?? `https://github.com/${fullName}`,source_query: sourceQuery,repo_relationship_hints: repoRelationshipHints,readme_excerpt: readme ? readme.slice(0, README_EXCERPT_LIMIT) : 'README unavailable',tree_truncated: treeTruncated,tree_truncation_note: treeTruncated? 'GitHub recursive tree response was truncated; relevant_paths and dataset/code clues may be incomplete.': undefined,relevant_paths: relevantPaths,support_file_excerpts: supportFileExcerpts,dataset_mentions: datasetMentions,dataset_candidate_evidence: datasetCandidateEvidence,issue_search_results: issues,}}function buildDatasetCluesFromRepos(repoEvidence: Array<Record<string, unknown> | undefined>): Array<Record<string, unknown>> {const clues: Array<Record<string, unknown>> = []for (const repo of repoEvidence) {if (!repo) continueconst paths = Array.isArray(repo['relevant_paths'])? repo['relevant_paths'].filter((pathValue): pathValue is string => typeof pathValue === 'string'): []const datasetPaths = paths.filter((pathValue) => /(^|\/)(data|datasets?|download|prepare|preprocess)/i.test(pathValue)).slice(0, DATASET_RELATED_PATH_LIMIT)const datasetMentions = Array.isArray(repo['dataset_mentions'])? repo['dataset_mentions'].filter((name): name is string => typeof name === 'string'): []const datasetCandidateEvidence = Array.isArray(repo['dataset_candidate_evidence'])? repo['dataset_candidate_evidence'].filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object').slice(0, DATASET_CANDIDATE_LIMIT_PER_REPO): []const supportFiles = Array.isArray(repo['support_file_excerpts'])? repo['support_file_excerpts'].filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object'): []const readme = typeof repo['readme_excerpt'] === 'string' ? repo['readme_excerpt'] : ''const datasetLines = readme.split(/\r?\n/).filter((line) => /dataset|data|download|benchmark|split|train|test/i.test(line)).slice(0, DATASET_CONTEXT_LINE_LIMIT)const supportFileDatasetLines = supportFiles.flatMap((item) => {const excerpt = typeof item['excerpt'] === 'string' ? item['excerpt'] : ''const filePath = typeof item['path'] === 'string' ? item['path'] : 'unknown file'return excerpt.split(/\r?\n|;/).filter((line) => /dataset|data_path|root_path|download|split|train|test/i.test(line)).slice(0, 2).map((line) => `${filePath}: ${compactText(line, 120)}`)}).slice(0, DATASET_CONTEXT_LINE_LIMIT * 2)if (datasetPaths.length > 0|| datasetLines.length > 0|| datasetMentions.length > 0|| datasetCandidateEvidence.length > 0|| supportFileDatasetLines.length > 0) {clues.push({repository: repo['full_name'],repository_url: repo['html_url'],repository_relationship_hints: summarizeRelationshipHints(repo['repo_relationship_hints']),target_tied_repository: repoIsTargetTied(repo),dataset_mentions: datasetMentions,dataset_candidate_evidence: datasetCandidateEvidence,dataset_related_paths: datasetPaths,readme_dataset_lines: datasetLines,support_file_dataset_lines: supportFileDatasetLines,})}}return clues}function collectRecordsWithTitle(value: unknown, acc: Array<Record<string, unknown>> = []): Array<Record<string, unknown>> {if (isUnavailableSourcePayload(value)) return accif (value === null || typeof value !== 'object') return accif (Array.isArray(value)) {for (const item of value) collectRecordsWithTitle(item, acc)return acc}const record = value as Record<string, unknown>if (typeof record['title'] === 'string' && record['title'].trim() !== '') {acc.push(record)}for (const [key, child] of Object.entries(record)) {if (NON_EVIDENCE_PAYLOAD_KEYS.has(key.toLowerCase())) continuecollectRecordsWithTitle(child, acc)}return acc}function isUnavailableSourcePayload(value: unknown): boolean {if (value === null || typeof value !== 'object' || Array.isArray(value)) return falseconst record = value as Record<string, unknown>return record['is_error'] === true || record['source_unavailable'] === true}const NON_EVIDENCE_PAYLOAD_KEYS = new Set(['args','arguments','count','incomplete_results','is_error','limit','message','next_cursor','note','offset','page','query','reason','request','source','source_unavailable','status','success','total','total_count','type',])function hasSuccessfulSourcePayload(value: unknown): boolean {if (value === null || value === undefined) return falseif (typeof value === 'string') return value.trim().length > 0if (typeof value === 'boolean') return valueif (typeof value === 'number') return Number.isFinite(value)if (Array.isArray(value)) return value.some(hasSuccessfulSourcePayload)if (typeof value !== 'object') return falseif (isUnavailableSourcePayload(value)) return falseconst evidenceEntries = Object.entries(value as Record<string, unknown>).filter(([key]) => !NON_EVIDENCE_PAYLOAD_KEYS.has(key.toLowerCase()))return evidenceEntries.some(([, child]) => hasSuccessfulSourcePayload(child))}function collectSourceEvidenceStrings(value: unknown, acc: string[] = []): string[] {if (isUnavailableSourcePayload(value)) return accif (typeof value === 'string') {acc.push(value)return acc}if (Array.isArray(value)) {for (const item of value) collectSourceEvidenceStrings(item, acc)return acc}if (value !== null && typeof value === 'object') {for (const [key, child] of Object.entries(value as Record<string, unknown>)) {if (NON_EVIDENCE_PAYLOAD_KEYS.has(key.toLowerCase())) continuecollectSourceEvidenceStrings(child, acc)}}return acc}function sourceEvidenceText(...values: unknown[]): string {return uniqueStrings(values.flatMap((value) => collectSourceEvidenceStrings(value).map((item) => compactText(item, 500))),120,).join('\n')}function hasReproductionFeedbackSignal(record: Record<string, unknown>): boolean {return /\b(?:reproduc|replicat|reimplement|re-implement|rerun|result mismatch|cannot reproduce|fail(?:ed)? to reproduce|metric mismatch|benchmark|evaluation protocol|code availability|data availability|artifact)\b/i.test(sourceEvidenceText(record))}function sourceHasReproductionFeedbackSignal(value: unknown): boolean {return collectRecordsWithTitle(value).some(hasReproductionFeedbackSignal)}function firstContextMatch(text: string, patterns: RegExp[], maxLength = 280): string | undefined {for (const pattern of patterns) {const match = pattern.exec(text)pattern.lastIndex = 0if (!match || match.index === undefined) continueconst start = Math.max(0, match.index - 140)const end = Math.min(text.length, match.index + match[0].length + 140)return compactText(text.slice(start, end), maxLength)}return undefined}function findDatasetRestrictionEvidence(text: string): string | undefined {const datasetContext = /\b(?:dataset|datasets|data set|split|splits|holdout|benchmark|leaderboard|labels?|annotations?|download|data access)\b/iconst restrictionSignal = /\b(?:restricted|requires? approval|approval[- ]?gated|apply|application|private|hidden|permission|not public|unavailable|non-commercial|noncommercial|terms of use)\b/iconst paperLicenseNotice = /\b(?:snippet is extracted|open access paper|paper or abstract|copyright owner|license by the author|source to verify the license|copyright information)\b/ifor (const segment of text.split(/\r?\n|(?<=[.!?])\s+/)) {const normalized = compactText(segment, 320)if (!normalized || paperLicenseNotice.test(normalized)) continueif (datasetContext.test(normalized) && restrictionSignal.test(normalized)) {return normalized}}return undefined}function extractGithubUrls(text: string): string[] {return uniqueStrings([...text.matchAll(/https?:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/g)].map((match) => match[0].replace(/[).,;]+$/g, '')),5,)}function extractMetricNames(text: string): string[] {return uniqueStrings([...text.matchAll(/\b(?:macro[- ]?F1|micro[- ]?F1|F1|accuracy|exact match|EM|BLEU|ROUGE|MSE|MAE|RMSE|AUROC|AUC)\b/gi)].map((match) => match[0]),8,)}function extractedClaim(claim: string,evidence: string | undefined,sourceDescription = 'Asta paper metadata or paper-scoped snippets',): Record<string, string> {return evidence? { status: 'found', claim, evidence }: {status: 'not_found_in_asta_sources',claim: `No explicit claim found in ${sourceDescription}.`,evidence: `Asta did not surface enough target-paper text from ${sourceDescription} for this claim type.`,}}function extractedUnscopedEvidence(evidenceType: string,evidence: string | undefined,): Record<string, string> {return evidence? {status: 'unscoped_evidence',evidence_scope: 'global_snippet_search',note: `${evidenceType} surfaced in global Asta snippets, but no paper_id was resolved; do not treat this as a target-paper claim without corroboration.`,evidence,}: {status: 'not_found_in_global_snippets',evidence_scope: 'global_snippet_search',note: `No ${evidenceType.toLowerCase()} surfaced in the unscoped Asta snippets.`,evidence: 'Asta global snippet_search did not return a matching evidence segment.',}}function extractLiveClaimEvidence(text: string) {const githubUrls = extractGithubUrls(text)const metricNames = extractMetricNames(text)const codeEvidence = githubUrls.length > 0? firstContextMatch(text, [/https?:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/i]): firstContextMatch(text, [/\b(?:code|source code|implementation|repository)\b.{0,120}\b(?:available|released|public|github)\b/i,/\b(?:available|released|public)\b.{0,120}\b(?:code|source code|implementation|repository)\b/i,])const datasetEvidence = firstContextMatch(text, [/\b(?:dataset|data|split|holdout|benchmark)\b.{0,160}\b(?:public|available|released|restricted|approval|license|download)\b/i,/\b(?:public|available|released|restricted|approval|license|download)\b.{0,160}\b(?:dataset|data|split|holdout|benchmark)\b/i,])const metricEvidence = firstContextMatch(text, [/\b(?:primary|main|reported|evaluation|metric)\b.{0,120}\b(?:macro[- ]?F1|micro[- ]?F1|F1|accuracy|exact match|EM|BLEU|ROUGE|MSE|MAE|RMSE|AUROC|AUC)\b/i,/\b(?:macro[- ]?F1|micro[- ]?F1|F1|accuracy|exact match|EM|BLEU|ROUGE|MSE|MAE|RMSE|AUROC|AUC)\b.{0,120}\b(?:primary|main|reported|evaluation|metric)\b/i,])const resultEvidence = firstContextMatch(text, [/\b\d+(?:\.\d+)?\s*(?:%|points?)?\s*(?:macro[- ]?F1|micro[- ]?F1|F1|accuracy|exact match|EM|BLEU|ROUGE|MSE|MAE|RMSE|AUROC|AUC)\b/i,/\b(?:macro[- ]?F1|micro[- ]?F1|F1|accuracy|exact match|EM|BLEU|ROUGE|MSE|MAE|RMSE|AUROC|AUC)\b.{0,40}\b\d+(?:\.\d+)?\b/i,])const computeEvidence = firstContextMatch(text, [/\b(?:single[- ]?GPU|GPU|TPU|A100|V100|H100|24GB|40GB|80GB|hours?|runtime|compute)\b.{0,140}\b(?:train|reproduce|experiment|run|finetune|fine[- ]?tune)\b/i,/\b(?:train|reproduce|experiment|run|finetune|fine[- ]?tune)\b.{0,140}\b(?:single[- ]?GPU|GPU|TPU|A100|V100|H100|24GB|40GB|80GB|hours?|runtime|compute)\b/i,])const reproducibilityEvidence = firstContextMatch(text, [/\b(?:reproducible|reproduce|replicate|all experiments|released code|public datasets?|artifact)\b/i,])return {githubUrls,metricNames,codeEvidence,datasetEvidence,metricEvidence,resultEvidence,computeEvidence,reproducibilityEvidence,}}function liveClaimFields(evidence: ReturnType<typeof extractLiveClaimEvidence>,sourceDescription: string,): Record<string, Record<string, string>> {return {code_availability: extractedClaim(evidence.githubUrls.length > 0? `Code or implementation URL appears in Asta evidence: ${evidence.githubUrls.join(', ')}.`: 'Code or implementation availability is mentioned in Asta evidence.',evidence.codeEvidence,sourceDescription,),dataset_availability: extractedClaim('Dataset, split, access, or benchmark availability is mentioned in Asta evidence.',evidence.datasetEvidence,sourceDescription,),primary_metric: extractedClaim(evidence.metricNames.length > 0? `Metric evidence mentions: ${evidence.metricNames.join(', ')}.`: 'Metric or evaluation protocol is mentioned in Asta evidence.',evidence.metricEvidence,sourceDescription,),reported_result: extractedClaim('Reported result evidence includes a numeric score tied to an evaluation metric.',evidence.resultEvidence,sourceDescription,),compute_claim: extractedClaim('Compute or runtime evidence is mentioned in Asta evidence.',evidence.computeEvidence,sourceDescription,),reproducibility_promise: extractedClaim('Reproducibility, released-artifact, or replication promise is mentioned in Asta evidence.',evidence.reproducibilityEvidence,sourceDescription,),}}function liveUnscopedEvidenceFields(evidence: ReturnType<typeof extractLiveClaimEvidence>,): Record<string, Record<string, string>> {return {code_availability_lead: extractedUnscopedEvidence('Code availability evidence', evidence.codeEvidence),dataset_availability_lead: extractedUnscopedEvidence('Dataset availability evidence', evidence.datasetEvidence),primary_metric_lead: extractedUnscopedEvidence('Metric evidence', evidence.metricEvidence),reported_result_lead: extractedUnscopedEvidence('Reported result evidence', evidence.resultEvidence),compute_claim_lead: extractedUnscopedEvidence('Compute/runtime evidence', evidence.computeEvidence),reproducibility_promise_lead: extractedUnscopedEvidence('Reproducibility promise evidence', evidence.reproducibilityEvidence),}}function buildLivePaperClaims(asta: {targetPaperMetadata: unknownclaimSnippets: unknowndatasetSnippets: unknownreplicationSnippets: unknown},resolvedPaperId: string | null | undefined,): Record<string, unknown> {const hasResolvedPaperId = typeof resolvedPaperId === 'string' && resolvedPaperId.trim() !== ''const hasTargetMetadata = hasSuccessfulSourcePayload(asta.targetPaperMetadata)const scopedText = hasResolvedPaperId? sourceEvidenceText(asta.targetPaperMetadata,asta.claimSnippets,asta.datasetSnippets,asta.replicationSnippets,): hasTargetMetadata? sourceEvidenceText(asta.targetPaperMetadata): ''const scopedSourceDescription = hasResolvedPaperId? hasTargetMetadata? 'validated Asta target-paper metadata and paper-scoped snippets': 'paper-scoped Asta snippets': hasTargetMetadata? 'validated Asta target-paper metadata only': 'no validated Asta target-paper metadata'const paperClaimFields = liveClaimFields(extractLiveClaimEvidence(scopedText), scopedSourceDescription)const unscopedSnippetText = hasResolvedPaperId? '': sourceEvidenceText(asta.claimSnippets, asta.datasetSnippets, asta.replicationSnippets)return {source_note: hasResolvedPaperId? 'LIVE claim surface constructed deterministically from validated Asta target-paper metadata when available plus paper-scoped snippet_search results; missing fields are explicit evidence gaps, not model guesses.': hasTargetMetadata? 'LIVE claim surface constructed deterministically from validated Asta target-paper metadata only. Asta snippets were global because no paper_id resolved, so snippet-derived evidence is surfaced separately as unscoped leads.': 'LIVE claim surface has no validated target-paper metadata. Asta snippets were global because no paper_id resolved, so snippet-derived evidence is surfaced separately as unscoped leads.',snippet_scope: {resolved_paper_id: hasResolvedPaperId ? resolvedPaperId : null,snippet_search_scope: hasResolvedPaperId ? 'paper_scoped' : 'global_unscoped',metadata_scope: hasTargetMetadata ? 'target_scoped' : 'unresolved_unscoped',claim_policy: hasResolvedPaperId? 'Snippet-derived evidence may be treated as target-paper evidence.': 'Snippet-derived evidence must not be treated as target-paper claims without corroboration.',},...paperClaimFields,...(!hasResolvedPaperId? {unscoped_snippet_evidence: {source_note: 'Global Asta snippet_search was used because paper_id resolution failed. Treat these entries as discovery leads or uncertainties, not paper claims.',...liveUnscopedEvidenceFields(extractLiveClaimEvidence(unscopedSnippetText)),},}: {}),}}function summarizeRelationshipHints(value: unknown): string[] {if (value === null || typeof value !== 'object') return []const labels: Record<string, string> = {linked_from_paper_metadata: 'linked from paper metadata',mentions_query_or_title: 'mentions query or title',mentions_official: 'official claim tied to target',official_claim_context_unclear: 'official claim not tied to target',explicit_non_official_claim: 'explicit non-official claim',mentions_reproduction: 'mentions reproduction',mentions_datasets: 'mentions datasets',}return Object.entries(value as Record<string, unknown>).filter(([, flag]) => flag === true).map(([key]) => labels[key] ?? key.replace(/_/g, ' '))}function scoreRepositoryForReplication(repo: Record<string, unknown>): number {const hints = repo['repo_relationship_hints'] !== null && typeof repo['repo_relationship_hints'] === 'object'? repo['repo_relationship_hints'] as Record<string, unknown>: {}const paths = Array.isArray(repo['relevant_paths'])? repo['relevant_paths'].filter((item): item is string => typeof item === 'string'): []const issues = Array.isArray(repo['issue_search_results'])? repo['issue_search_results'].filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object'): []let score = 0if (repo['source_query'] === 'github-url-from-paper-metadata') score += 120if (hints['linked_from_paper_metadata'] === true) score += 60if (hints['mentions_query_or_title'] === true) score += 80if (hints['mentions_official'] === true) score += 55if (hints['official_claim_context_unclear'] === true) score -= 12if (hints['explicit_non_official_claim'] === true) score -= 35if (hints['mentions_reproduction'] === true) score += 20if (hints['mentions_datasets'] === true) score += 15if (paths.some((pathValue) => /(^|\/)(scripts?|experiments?)\//i.test(pathValue))) score += 25if (paths.some((pathValue) => /(run|train|long|forecast|eval|metric).*\.(py|sh)$/i.test(pathValue))) score += 20if (paths.some((pathValue) => /(^|\/)(data_provider|data_loader|datasets?)\//i.test(pathValue))) score += 15const issueTitles = issues.map((issue) => String(issue['title'] ?? '')).join(' ')if (/reproduc|replicat|result|hyperparameter|table/i.test(issueTitles)) score += 15const description = String(repo['description'] ?? '')const readme = String(repo['readme_excerpt'] ?? '')const genericText = `${description}\n${readme}`if (/framework|toolkit|library|baseline/i.test(genericText) && hints['mentions_query_or_title'] !== true) {score -= 20}const stars = getNumber(repo, 'stargazers_count') ?? 0score += Math.min(10, Math.log10(stars + 1) * 3)return score}function sortRepositoryEvidence(repos: Array<Record<string, unknown>>): Array<Record<string, unknown>> {return [...repos].sort((a, b) => {const scoreDelta = scoreRepositoryForReplication(b) - scoreRepositoryForReplication(a)if (scoreDelta !== 0) return scoreDeltareturn String(a['full_name'] ?? '').localeCompare(String(b['full_name'] ?? ''))})}function compactRepositoryEvidenceForCodeAgent(repo: Record<string, unknown>): Record<string, unknown> {const {dataset_candidate_evidence: _datasetCandidateEvidence,dataset_mentions: _datasetMentions,...rest} = reporeturn rest}function repoText(repo: Record<string, unknown>): string {return sourceEvidenceText(repo['full_name'],repo['description'],repo['readme_excerpt'],repo['relevant_paths'],repo['support_file_excerpts'],repo['issue_search_results'],)}function repoHasUsableSnapshot(repo: Record<string, unknown>): boolean {const readme = String(repo['readme_excerpt'] ?? '')const paths = Array.isArray(repo['relevant_paths']) ? repo['relevant_paths'] : []return readme !== '' && readme !== 'README unavailable' || paths.length > 0}function repoHasTrainingOrEvalPath(repo: Record<string, unknown>): boolean {const paths = Array.isArray(repo['relevant_paths'])? repo['relevant_paths'].filter((item): item is string => typeof item === 'string'): []return paths.some((pathValue) => /(train|training|eval|evaluation|metric|run|launch).*\.(py|sh|yaml|yml|json)$/i.test(pathValue))}function repoHasHint(repo: Record<string, unknown>, key: string): boolean {const hints = repo['repo_relationship_hints']return hints !== null && typeof hints === 'object' && (hints as Record<string, unknown>)[key] === true}function repoIsTargetTied(repo: Record<string, unknown>): boolean {return repo['source_query'] === 'github-url-from-paper-metadata'|| repoHasHint(repo, 'linked_from_paper_metadata')|| repoHasHint(repo, 'mentions_official')|| repoHasHint(repo, 'mentions_query_or_title')}function repoHasUnofficialImplementationSignal(repo: Record<string, unknown>): boolean {if (repoHasHint(repo, 'explicit_non_official_claim')) return trueconst text = repoText(repo)return /\b(?:(?:un|non)[-\s]?official|third[-\s]?party|community|independent)\s+(?:implementation|code|repo(?:sitory)?|release|reproduction)\b/i.test(text)|| /\b(?:reimplementation|re-implementation)\b/i.test(text)|| /\b(?:replication|reproduction)\s+(?:implementation|code|repo(?:sitory)?)\b/i.test(text)}function statusItem(artifactType: string,status: string,source: string,note: string,evidenceIds: string[] = [],): Record<string, unknown> {return {artifact_type: artifactType,status,source,note,evidence_ids: evidenceIds,}}function buildLiveDiscoveryStatus(input: {asta: {targetPaperMetadata: unknowncitations: unknowndatasetSnippets: unknownreplicationSnippets: unknown}snippetScope: SnippetSearchScoperepoEvidence: Array<Record<string, unknown>>datasetClues: Array<Record<string, unknown>>directRepos: string[]}): Array<Record<string, unknown>> {const { asta, snippetScope, repoEvidence, datasetClues, directRepos } = inputconst snippetsArePaperScoped = snippetScope === 'paper_scoped'const scholarlyMetadataFound =hasSuccessfulSourcePayload(asta.targetPaperMetadata) && findFirstStringByKey(asta.targetPaperMetadata, 'title') !== undefinedconst officialRepos = repoEvidence.filter((repo) =>repo['source_query'] === 'github-url-from-paper-metadata'|| repoHasHint(repo, 'linked_from_paper_metadata')|| repoHasHint(repo, 'mentions_official'),)const targetTiedRepos = repoEvidence.filter(repoIsTargetTied)const usableOfficialRepos = officialRepos.filter(repoHasUsableSnapshot)const unofficialRepos = repoEvidence.filter((repo) =>!officialRepos.includes(repo)&& repoHasUnofficialImplementationSignal(repo),)const relatedBenchmarkRepos = repoEvidence.filter((repo) =>!officialRepos.includes(repo)&& !unofficialRepos.includes(repo)&& /benchmark|baseline|toolkit|framework|library/i.test(repoText(repo)),)const anyRepoFetched = repoEvidence.length > 0const scopedDatasetSnippets = snippetsArePaperScoped ? asta.datasetSnippets : undefinedconst scopedReplicationSnippets = snippetsArePaperScoped ? asta.replicationSnippets : undefinedconst unscopedDatasetSnippetsFound =!snippetsArePaperScoped && hasSuccessfulSourcePayload(asta.datasetSnippets)const unscopedReplicationSnippetsFound =!snippetsArePaperScoped && hasSuccessfulSourcePayload(asta.replicationSnippets)const targetTiedDatasetClues = datasetClues.filter((clue) => clue['target_tied_repository'] === true)const untiedDatasetCluesFound = datasetClues.length > 0 && targetTiedDatasetClues.length === 0const targetTiedEntrypointRepos = targetTiedRepos.filter(repoHasTrainingOrEvalPath)const untiedEntrypointReposFound =targetTiedEntrypointRepos.length === 0 && repoEvidence.some(repoHasTrainingOrEvalPath)const datasetEvidenceFound =hasSuccessfulSourcePayload(scopedDatasetSnippets) || targetTiedDatasetClues.length > 0const datasetText = sourceEvidenceText(scopedDatasetSnippets, targetTiedDatasetClues)const restrictedDatasetEvidence = findDatasetRestrictionEvidence(datasetText)const metricPattern = /\b(?:macro[- ]?F1|micro[- ]?F1|F1|accuracy|exact match|EM|BLEU|ROUGE|MSE|MAE|RMSE|AUROC|AUC|metric|evaluator|evaluation)\b/iconst metricProtocolEvidence = firstContextMatch(sourceEvidenceText(scopedDatasetSnippets, scopedReplicationSnippets, targetTiedRepos), [metricPattern,])const untiedMetricEvidence = !metricProtocolEvidence && firstContextMatch(sourceEvidenceText(repoEvidence), [metricPattern,])const citationTraversalFound = hasSuccessfulSourcePayload(asta.citations)const citationFeedbackSignalFound = sourceHasReproductionFeedbackSignal(asta.citations)const citationFeedbackFound =citationFeedbackSignalFound || hasSuccessfulSourcePayload(scopedReplicationSnippets)return [statusItem('scholarly_metadata',scholarlyMetadataFound ? 'found' : 'source_unavailable','Asta MCP get_paper/search_paper_by_title',scholarlyMetadataFound? 'Resolved validated target-paper metadata from Asta.': 'Asta paper lookup did not return a validated target-paper metadata payload.',),statusItem('official_code',usableOfficialRepos.length > 0? 'found': officialRepos.length > 0 || directRepos.length > 0? 'missing': anyRepoFetched? 'ambiguous': 'source_unavailable','GitHub URLs from paper metadata plus GitHub Search README/tree inspection',usableOfficialRepos.length > 0? 'At least one candidate official repository has a readable README or relevant file tree.': officialRepos.length > 0 || directRepos.length > 0? 'A paper-linked or official-looking repository was found, but README/tree evidence was unavailable or unusable.': anyRepoFetched? 'GitHub found code-like candidates, but none could be tied to official ownership.': 'No repository evidence could be fetched from GitHub.',officialRepos.map((repo) => String(repo['full_name'] ?? repo['html_url'] ?? '')).filter((item) => item !== ''),),statusItem('unofficial_code',unofficialRepos.length > 0 ? 'found' : anyRepoFetched ? 'not_required_for_replication' : 'source_unavailable','GitHub Search README/tree/issues',unofficialRepos.length > 0? 'At least one candidate repository presents as an unofficial reproduction or reimplementation.': anyRepoFetched? 'Fetched repositories did not clearly identify as unofficial reproductions; this is not a missing replication artifact unless another source requires a third-party reproduction.': 'No repository evidence could be fetched from GitHub.',unofficialRepos.map((repo) => String(repo['full_name'] ?? repo['html_url'] ?? '')).filter((item) => item !== ''),),statusItem('related_benchmark_code',relatedBenchmarkRepos.length > 0 ? 'found' : anyRepoFetched ? 'not_required_for_replication' : 'source_unavailable','GitHub Search README/tree/issues',relatedBenchmarkRepos.length > 0? 'At least one candidate appears to be related benchmark, baseline, toolkit, or framework code rather than the paper method itself.': anyRepoFetched? 'Fetched repositories did not clearly look like related benchmark/baseline code; this is not a missing replication artifact unless the paper requires a separate baseline repository.': 'No repository evidence could be fetched from GitHub.',relatedBenchmarkRepos.map((repo) => String(repo['full_name'] ?? repo['html_url'] ?? '')).filter((item) => item !== ''),),statusItem('training_or_eval_entrypoint',targetTiedEntrypointRepos.length > 0? 'found': untiedEntrypointReposFound? 'ambiguous': anyRepoFetched? 'missing': 'source_unavailable','GitHub recursive tree and support-file excerpts',targetTiedEntrypointRepos.length > 0? 'At least one target-tied candidate repository exposes train/eval/run/metric paths.': untiedEntrypointReposFound? 'Only non-target-tied fetched repositories expose train/eval/run/metric paths; treat as ambiguous, not as target-paper entrypoint evidence.': anyRepoFetched? 'No train/eval/run/metric path was visible in fetched repository snapshots.': 'No repository evidence could be fetched from GitHub.',(targetTiedEntrypointRepos.length > 0 ? targetTiedEntrypointRepos : repoEvidence.filter(repoHasTrainingOrEvalPath)).map((repo) => String(repo['full_name'] ?? repo['html_url'] ?? '')).filter((item) => item !== ''),),statusItem('dataset_candidate_evidence',targetTiedDatasetClues.length > 0? 'found': datasetEvidenceFound || untiedDatasetCluesFound || unscopedDatasetSnippetsFound? 'ambiguous': 'source_unavailable','Asta MCP snippet_search plus dataset clues from repository READMEs/paths',targetTiedDatasetClues.length > 0? 'Target-tied repository README/path/support-file clues surfaced concrete dataset candidates.': datasetEvidenceFound? 'Asta surfaced dataset-related snippets, but repository candidate evidence is absent or not concrete.': untiedDatasetCluesFound? 'Only non-target-tied repositories surfaced dataset candidates; treat them as leads, not target-paper dataset facts.': unscopedDatasetSnippetsFound? 'Only global/unscoped Asta dataset snippets were available; treat them as leads, not target-paper dataset facts.': 'No dataset evidence was available from Asta snippets or repository clues.',(targetTiedDatasetClues.length > 0 ? targetTiedDatasetClues : datasetClues).map((clue) => String(clue['repository'] ?? clue['repository_url'] ?? '')).filter((item) => item !== ''),),statusItem('restricted_or_private_dataset',restrictedDatasetEvidence? 'restricted': datasetEvidenceFound? 'no_restriction_signal': untiedDatasetCluesFound || unscopedDatasetSnippetsFound? 'ambiguous': 'source_unavailable','Asta MCP dataset snippet_search and repository dataset clues',restrictedDatasetEvidence? `Access restriction signal surfaced: ${restrictedDatasetEvidence}`: datasetEvidenceFound? 'No restricted/private/approval-gated dataset signal was visible in the live evidence snapshot.': untiedDatasetCluesFound? 'Only non-target-tied repository dataset clues were available, so no target-paper restriction status is asserted.': unscopedDatasetSnippetsFound? 'Only global/unscoped dataset snippets were available, so no target-paper restriction status is asserted.': 'No dataset evidence was available to inspect access restrictions.',),statusItem('metric_protocol',metricProtocolEvidence? 'found': datasetEvidenceFound || citationFeedbackFound || untiedMetricEvidence? 'ambiguous': 'source_unavailable','Asta MCP snippet_search plus repository metric/eval paths',metricProtocolEvidence? `Target-scoped metric/evaluator signal surfaced: ${metricProtocolEvidence}`: untiedMetricEvidence? `Only non-target-tied repository metric/evaluator signal surfaced: ${untiedMetricEvidence}`: datasetEvidenceFound || citationFeedbackFound? 'Evidence exists, but no concrete target-scoped metric/evaluator signal was surfaced.': 'No dataset or reproduction evidence was available to inspect metrics.',),statusItem('citation_feedback',citationFeedbackFound? 'found': citationTraversalFound || unscopedReplicationSnippetsFound? 'ambiguous': 'source_unavailable',snippetsArePaperScoped? 'Asta MCP get_citations and paper-scoped snippet_search': 'Asta MCP get_citations; global snippet_search is not treated as citation feedback',citationFeedbackFound? 'Citation traversal with explicit reproduction-feedback signals or paper-scoped reproduction snippets returned evidence.': citationTraversalFound? 'Citation traversal returned papers, but no explicit reproduction/metric/result/code/data feedback signal was visible in the live snapshot.': unscopedReplicationSnippetsFound? 'Global reproduction snippets were available but no paper_id resolved; they are unscoped leads, not target-paper citation feedback.': 'Asta citation traversal and reproduction snippets were unavailable.',),]}function buildLiveSourceDigest(query: string,asta: {targetPaperMetadata: unknowncitations: unknowndatasetSnippets: unknownreplicationSnippets: unknown},snippetScope: SnippetSearchScope,repoEvidence: Array<Record<string, unknown>>,datasetClues: Array<Record<string, unknown>>,): SourceDigest {const snippetsArePaperScoped = snippetScope === 'paper_scoped'const paperTitle = findFirstStringByKey(asta.targetPaperMetadata, 'title') ?? queryconst paperUrl = findFirstStringByKey(asta.targetPaperMetadata, 'url') ?? ''const paperVenue = findFirstStringByKey(asta.targetPaperMetadata, 'venue') ?? ''const paperYear = findFirstNumberByKey(asta.targetPaperMetadata, 'year')const repositories = repoEvidence.slice(0, REPO_EVIDENCE_LIMIT).map((repo) => {const issues = Array.isArray(repo['issue_search_results'])? repo['issue_search_results'].filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object'): []const paths = Array.isArray(repo['relevant_paths'])? repo['relevant_paths'].filter((item): item is string => typeof item === 'string'): []return {repository: String(repo['full_name'] ?? ''),url: String(repo['html_url'] ?? ''),description: String(repo['description'] ?? ''),relationship_hints: summarizeRelationshipHints(repo['repo_relationship_hints']),useful_paths: paths.slice(0, 8),issue_signals: issues.slice(0, 4).map((issue) => ({title: String(issue['title'] ?? ''),url: String(issue['html_url'] ?? ''),})),}})const datasetByName = new Map<string, SourceDigest['datasets'][number]>()for (const clue of datasetClues) {const source = String(clue['repository_url'] ?? clue['repository'] ?? '')const mentions = Array.isArray(clue['dataset_mentions'])? clue['dataset_mentions'].filter((item): item is string => typeof item === 'string'): []const candidateEvidence = Array.isArray(clue['dataset_candidate_evidence'])? clue['dataset_candidate_evidence'].filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object'): []const candidateMentions = candidateEvidence.map((item) => String(item['name'] ?? '')).filter((item) => item !== '')for (const name of uniqueStrings([...mentions, ...candidateMentions], 20)) {if (datasetByName.has(name)) continueconst evidenceItem = candidateEvidence.find((item) => String(item['name'] ?? '').toLowerCase() === name.toLowerCase())const evidenceKind = evidenceItem ? String(evidenceItem['evidence_kind'] ?? '') : ''const evidenceLine = evidenceItem ? String(evidenceItem['evidence'] ?? '') : ''const hasEvidence = evidenceKind !== '' || evidenceLine !== ''datasetByName.set(name, {name,evidence: hasEvidence? `Detected from scored candidate evidence (${evidenceKind}): ${compactText(evidenceLine, 140)}`: 'Detected from repository README, script/config path, or support-file excerpt.',source,})}}const datasetSnippetMentions = snippetsArePaperScoped? extractDatasetMentionsFromAstaSnippets(asta.datasetSnippets): []const reproductionByKey = new Map<string, SourceDigest['reproduction_signals'][number]>()const targetTitles = new Set([normalizeTitleForCompare(query),normalizeTitleForCompare(paperTitle),])for (const repo of repoEvidence) {const issues = Array.isArray(repo['issue_search_results'])? repo['issue_search_results'].filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object'): []for (const issue of issues) {const title = String(issue['title'] ?? '')const url = String(issue['html_url'] ?? '')if (!title || reproductionByKey.has(url || title)) continuereproductionByKey.set(url || title, { title, url, source: 'GitHub issue search' })}}for (const item of [...collectRecordsWithTitle(asta.citations),...(snippetsArePaperScoped ? collectRecordsWithTitle(asta.replicationSnippets) : []),]) {const title = String(item['title'] ?? '')const url = getString(item, 'url') ?? getString(item, 'paperUrl') ?? ''const normalizedTitle = normalizeTitleForCompare(title)const isToolError = isUnavailableSourcePayload(item)if (isToolError || targetTitles.has(normalizedTitle) || /^asta .*retrieval$/i.test(title)) continueif (!hasReproductionFeedbackSignal(item)) continueif (!title || reproductionByKey.has(url || title)) continuereproductionByKey.set(url || title, {title,url,source: 'Asta citation/snippet evidence',})if (reproductionByKey.size >= 10) break}return {paper: {title: paperTitle,url: paperUrl,venue_or_year: [paperVenue, paperYear].filter((item) => item !== undefined && item !== '').join(' '),},repositories,datasets: Array.from(datasetByName.values()).slice(0, 20),dataset_snippet_mentions: datasetSnippetMentions,reproduction_signals: Array.from(reproductionByKey.values()).slice(0, 10),}}function normalizePaperId(query: string): string | undefined {const trimmed = query.trim()if (/^(DOI|ARXIV|PMID|PMCID|MAG|ACL|CorpusId|URL):/i.test(trimmed)) {return trimmed.replace(/^doi:/i, 'DOI:').replace(/^arxiv:/i, 'ARXIV:').replace(/^pmid:/i, 'PMID:').replace(/^pmcid:/i, 'PMCID:').replace(/^mag:/i, 'MAG:').replace(/^acl:/i, 'ACL:').replace(/^corpusid:/i, 'CorpusId:').replace(/^url:/i, 'URL:')}if (/^10\.\d{4,9}\//.test(trimmed)) {return `DOI:${trimmed}`}if (/^\d{4}\.\d{4,5}(v\d+)?$/i.test(trimmed)) {return `ARXIV:${trimmed}`}if (/^https?:\/\/(arxiv\.org|www\.semanticscholar\.org|aclanthology\.org|dl\.acm\.org)/i.test(trimmed)) {return `URL:${trimmed}`}return undefined}function paperIdFromRecord(record: Record<string, unknown>): string | undefined {for (const key of ['paper_id', 'paperId', 'paperID', 'corpusId', 'CorpusId']) {const candidate = record[key]if (typeof candidate === 'string' && candidate.trim() !== '') {if (key.toLowerCase().includes('corpus') && !candidate.startsWith('CorpusId:')) {return `CorpusId:${candidate}`}return candidate}if (typeof candidate === 'number') {return `CorpusId:${candidate}`}}return undefined}function significantTitleTokens(text: string): string[] {const stopwords = new Set(['a','an','and','are','as','at','by','for','from','in','is','of','on','or','the','to','with',])return normalizeTitleForCompare(text).split(' ').filter((token) => token.length > 2 && !stopwords.has(token))}function titleMatchesQuery(candidateTitle: string, query: string): boolean {const candidate = normalizeTitleForCompare(candidateTitle)const target = normalizeTitleForCompare(query)if (!candidate || !target) return falseif (candidate === target) return trueconst targetTokens = significantTitleTokens(query)if (targetTokens.length < 3) return falseconst candidateTokens = significantTitleTokens(candidateTitle)if (candidateTokens.length < 3) return falseconst candidateTokenSet = new Set(candidateTokens)const overlap = targetTokens.filter((token) => candidateTokenSet.has(token)).lengthconst targetCoverage = overlap / targetTokens.lengthconst candidateCoverage = overlap / candidateTokens.lengthreturn targetCoverage >= 0.82 && candidateCoverage >= 0.7}function resolveLivePaperIdentity(query: string,knownPaperId: string | undefined,paperLookup: unknown,): LivePaperIdentity {if (knownPaperId) {return {resolvedPaperId: knownPaperId,targetPaperMetadata: hasSuccessfulSourcePayload(paperLookup) ? paperLookup : undefined,metadataScope: hasSuccessfulSourcePayload(paperLookup) ? 'target_scoped' : 'unresolved_unscoped',note: hasSuccessfulSourcePayload(paperLookup)? 'Input was an explicit paper identifier, so Asta get_paper metadata is treated as target-scoped.': 'Input was an explicit paper identifier, but Asta get_paper did not return usable metadata. Snippet and citation requests can still be scoped by the input identifier.',}}for (const record of collectRecordsWithTitle(paperLookup)) {const title = getString(record, 'title')if (!title || !titleMatchesQuery(title, query)) continueconst paperId = paperIdFromRecord(record)return {...(paperId ? { resolvedPaperId: paperId } : {}),targetPaperMetadata: record,metadataScope: 'target_scoped',note: paperId? 'Asta title lookup returned a strict title match with a paper_id; snippets are paper-scoped.': 'Asta title lookup returned a strict title match but no paper_id; metadata is target-scoped, but snippets remain global/unscoped.',}}return {metadataScope: 'unresolved_unscoped',note: 'Asta title lookup did not return a strict target-paper match. Raw lookup payload is omitted from source-agent snapshots to avoid treating fuzzy search results as target-paper facts.',}}function toolResultToPlainObject(result: unknown): unknown {if (result === null || typeof result !== 'object') return resultconst record = result as Record<string, unknown>if (record['structuredContent'] !== undefined) {return record['structuredContent']}if (record['toolResult'] !== undefined) {return record['toolResult']}const content = record['content']if (Array.isArray(content)) {const textBlocks = content.map((block) => {if (block !== null && typeof block === 'object' && typeof (block as Record<string, unknown>)['text'] === 'string') {return (block as Record<string, unknown>)['text'] as string}return undefined}).filter((text): text is string => text !== undefined)if (textBlocks.length === 1) {try {return JSON.parse(textBlocks[0])} catch {return textBlocks[0]}}if (textBlocks.length > 1) {return textBlocks}}return result}async function withAstaClient<T>(run: (client: MCPClientLike) => Promise<T>): Promise<T> {const apiKey = process.env.ASTA_API_KEY?.trim()if (!apiKey) {throw new Error('SOURCE_MODE=live requires ASTA_API_KEY.')}let Client: MCPClientConstructorlet StreamableHTTPClientTransport: StreamableHTTPClientTransportConstructortry {const [clientModule, transportModule] = await Promise.all([import('@modelcontextprotocol/sdk/client/index.js') as Promise<{ Client: MCPClientConstructor }>,import('@modelcontextprotocol/sdk/client/streamableHttp.js') as Promise<{StreamableHTTPClientTransport: StreamableHTTPClientTransportConstructor}>,])Client = clientModule.ClientStreamableHTTPClientTransport = transportModule.StreamableHTTPClientTransport} catch (error) {const message = error instanceof Error ? error.message : String(error)throw new Error('SOURCE_MODE=live requires the optional @modelcontextprotocol/sdk peer dependency. ' +'Install it with `npm install @modelcontextprotocol/sdk` before using live Asta discovery. ' +`Original error: ${message}`,)}const transport = new StreamableHTTPClientTransport(new URL('https://asta-tools.allen.ai/mcp/v1'),{ requestInit: { headers: { 'x-api-key': apiKey } } },)const client = new Client({ name: 'open-multi-agent-paper-replication-triage', version: '0.0.0' },{ capabilities: {} },)await client.connect(transport, { timeout: 60_000 })try {return await run(client)} finally {await client.close?.()}}async function callAstaTool(client: MCPClientLike,name: string,args: Record<string, unknown>,): Promise<unknown> {try {const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 60_000 })return toolResultToPlainObject(result)} catch (error) {return {is_error: true,tool: name,args,message: error instanceof Error ? error.message : String(error),}}}async function loadLiveSourceBundle(query: string): Promise<SourceBundle> {const githubHeaders = process.env.GITHUB_TOKEN? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }: {}const astaFields = 'title,year,authors,venue,tldr,url,abstract,isOpenAccess,publicationDate,fieldsOfStudy'const knownPaperId = normalizePaperId(query)const asta = await withAstaClient(async (client) => {const paperLookup = knownPaperId? await callAstaTool(client, 'get_paper', { paper_id: knownPaperId, fields: astaFields }): await callAstaTool(client, 'search_paper_by_title', { title: query, fields: astaFields })const paperIdentity = resolveLivePaperIdentity(query, knownPaperId, paperLookup)const resolvedPaperId = paperIdentity.resolvedPaperIdconst paperIdsArg = resolvedPaperId ? { paper_ids: resolvedPaperId } : {}const snippetScope: SnippetSearchScope = resolvedPaperId ? 'paper_scoped' : 'global_unscoped'const [citations, claimSnippets, datasetSnippets, replicationSnippets] = await Promise.all([resolvedPaperId? callAstaTool(client, 'get_citations', {paper_id: resolvedPaperId,fields: 'title,year,authors,venue,tldr,url,abstract',limit: 10,}): Promise.resolve({source_unavailable: true,reason: 'Could not resolve a paper_id for citation traversal.',}),callAstaTool(client, 'snippet_search', {query: `${query} code implementation repository dataset split public metric result GPU reproducible released artifact`,limit: 5,...paperIdsArg,}),callAstaTool(client, 'snippet_search', {query: `${query} dataset split license benchmark holdout public restricted`,limit: 5,...paperIdsArg,}),callAstaTool(client, 'snippet_search', {query: `${query} reproduce replication reproduced results code metric mismatch benchmark`,limit: 5,...paperIdsArg,}),])return {paperLookupResolution: {resolved_paper_id: resolvedPaperId ?? null,metadata_scope: paperIdentity.metadataScope,note: paperIdentity.note,},targetPaperMetadata: paperIdentity.targetPaperMetadata ?? null,resolvedPaperId: resolvedPaperId ?? null,snippetScope,citations,claimSnippets,datasetSnippets,replicationSnippets,}})const githubSearchQueries = buildGitHubSearchQueries(query, asta.targetPaperMetadata)const githubSearches = await Promise.all(githubSearchQueries.map((rawQuery) => fetchGitHubSearch(rawQuery, githubHeaders)),)const githubItemByName = new Map<string, Record<string, unknown>>()for (const search of githubSearches) {if (!Array.isArray(search?.['items'])) continuefor (const item of search['items'] as Array<Record<string, unknown>>) {const fullName = getString(item, 'full_name')if (fullName && !githubItemByName.has(fullName)) {githubItemByName.set(fullName, item)}}}const directRepos = extractGithubRepoFullNames(asta.targetPaperMetadata)const githubItems = Array.from(githubItemByName.values())const directRepoInputs = directRepos.slice(0, REPO_DISCOVERY_FETCH_LIMIT)const directRepoNames = new Set(directRepoInputs.map((repo) => repo.toLowerCase()))const searchRepoInputs = githubItems.filter((item) => {const fullName = getString(item, 'full_name')return fullName ? !directRepoNames.has(fullName.toLowerCase()) : false}).slice(0, Math.max(0, REPO_DISCOVERY_FETCH_LIMIT - directRepoInputs.length))const repoEvidence = await Promise.all([...directRepoInputs.map((repo) => fetchGitHubRepoEvidence(repo, githubHeaders, 'github-url-from-paper-metadata')),...searchRepoInputs.map((item) => fetchGitHubRepoEvidence(item, githubHeaders, query)),],)const fullRepoEvidence = sortRepositoryEvidence(repoEvidence.filter((item): item is Record<string, unknown> => item !== undefined),)const snippetScope = asta.snippetScope as SnippetSearchScopeconst topRepoEvidence = fullRepoEvidence.slice(0, REPO_EVIDENCE_LIMIT)const datasetCluesFromRepos = buildDatasetCluesFromRepos(fullRepoEvidence)const sourceDigest = buildLiveSourceDigest(query, asta, snippetScope, fullRepoEvidence, datasetCluesFromRepos)const paperClaims = buildLivePaperClaims(asta, asta.resolvedPaperId)const discoveryStatus = buildLiveDiscoveryStatus({asta,snippetScope,repoEvidence: fullRepoEvidence,datasetClues: datasetCluesFromRepos,directRepos,})const searchTotal = githubSearches.reduce((sum, search) => {const count = typeof search?.['total_count'] === 'number' ? search['total_count'] : 0return sum + count}, 0)const searchIncomplete = githubSearches.some((search) => search?.['incomplete_results'] === true)return {mode: 'live',query,sourceIndex: {notice: 'LIVE BEST-EFFORT SOURCE DISCOVERY. Scholarly evidence comes from Asta MCP; implementation evidence comes from GitHub Search. Missing sources are evidence, not a request for manual input.',query,source_mode: 'live',discovery_status: discoveryStatus,},scholarlyMetadata: {notice: 'LIVE Asta MCP scholarly metadata. Asta exposes the Semantic Scholar academic graph over MCP.',query,paper_lookup_resolution: asta.paperLookupResolution,resolved_paper_id: asta.resolvedPaperId,asta_paper_lookup: hasSuccessfulSourcePayload(asta.targetPaperMetadata)? asta.targetPaperMetadata: {status: 'unresolved_unscoped_omitted',note: 'Raw Asta paper lookup was not exposed to the claim agent because it was not validated as the target paper.',},claim_snippet_scope: snippetScope,...(snippetScope === 'paper_scoped'? { asta_claim_snippets: asta.claimSnippets }: {unscoped_claim_snippets: {status: 'global_unscoped_omitted_from_claim_extraction',note: 'Asta claim snippet_search ran globally because no validated paper_id resolved. Raw snippets are omitted from the claim-agent snapshot so they cannot be mistaken for target-paper claims; see paper_claims.unscoped_snippet_evidence for discovery leads.',},}),paper_claims: paperClaims,},codeArtifacts: {notice: 'LIVE GitHub Search plus shallow repository evidence. GITHUB_TOKEN is optional but recommended to avoid low anonymous rate limits.',github_search_queries: githubSearchQueries,github_repos_linked_from_paper_metadata: directRepos,github_search_summary: {total_count_across_queries: searchTotal,incomplete_results_seen: searchIncomplete,top_results: githubItems.slice(0, REPO_EVIDENCE_LIMIT).map(compactSearchItem),},top_repository_evidence: topRepoEvidence.map(compactRepositoryEvidenceForCodeAgent),},datasetArtifacts: {notice: 'LIVE dataset evidence from Asta snippet_search plus dataset-related README/path clues from candidate repositories. This avoids requiring a separate dataset-catalog API key.',query: `${query} dataset split license benchmark holdout public restricted`,dataset_snippet_scope: snippetScope,...(snippetScope === 'paper_scoped'? { asta_dataset_snippets: asta.datasetSnippets }: {unscoped_dataset_snippets: {status: 'global_unscoped_omitted_from_dataset_facts',note: 'Asta dataset snippet_search ran globally because no validated paper_id resolved. Raw snippets are omitted from dataset facts; rely on repository-derived dataset clues or treat missing dataset evidence as an evidence gap.',},}),candidate_repositories_considered: fullRepoEvidence.length,dataset_clues_from_candidate_repositories: datasetCluesFromRepos,},citationFeedback: {notice: 'LIVE Asta citation and snippet evidence.',asta_citations: asta.citations,replication_snippet_scope: snippetScope,...(snippetScope === 'paper_scoped'? { asta_replication_snippets: asta.replicationSnippets }: {unscoped_replication_snippets: {status: 'global_unscoped_omitted_from_citation_feedback',note: 'Asta reproduction snippet_search ran globally because no validated paper_id resolved. Raw snippets are omitted from citation feedback so they cannot be mistaken for target-paper follow-up evidence.',},}),},sourceDigest,}}async function loadSourceBundle(query: string): Promise<SourceBundle> {const mode = resolveSourceMode()if (mode === 'live') {return loadLiveSourceBundle(query)}return loadMockSourceBundle(query)}// ---------------------------------------------------------------------------// Zod schemas// ---------------------------------------------------------------------------const EvidenceTopic = z.enum(['code', 'dataset', 'metric', 'result', 'artifact_missing'])const Severity = z.enum(['low', 'medium', 'high'])const RepositoryRelationship = z.enum(['likely_official', 'third_party', 'related_benchmark', 'unrelated', 'unclear'])const DatasetAccessStatus = z.enum(['public', 'restricted', 'unavailable', 'unclear'])const ClaimReport = z.object({source: z.literal('scholarly_metadata'),claims: z.array(z.object({topic: EvidenceTopic,claim: z.string(),evidence: z.string(),}),),reproduction_promises: z.array(z.string()),uncertainties: z.array(z.string()),})type ClaimReport = z.infer<typeof ClaimReport>const CodeArtifactReport = z.object({source: z.literal('code_artifacts'),official_code_found: z.boolean(),candidate_repositories: z.array(z.object({repository: z.string(),url: z.string(),relationship: RepositoryRelationship,evidence: z.string(),useful_paths: z.array(z.string()),issue_signals: z.array(z.object({title: z.string(),url: z.string(),evidence: z.string(),}),),}),),usable_training_entrypoint: z.boolean(),missing_files: z.array(z.string()),conflicts: z.array(z.object({topic: z.enum(['code', 'metric']),paper_claim: z.string(),artifact_evidence: z.string(),severity: Severity,}),),reproduction_notes: z.array(z.string()),})type CodeArtifactReport = z.infer<typeof CodeArtifactReport>const DatasetArtifactReport = z.object({source: z.literal('dataset_artifacts'),datasets: z.array(z.object({name: z.string(),source_or_url: z.string(),access_status: DatasetAccessStatus,split_or_license_notes: z.string(),evidence: z.string(),}),),public_splits: z.array(z.string()),restricted_splits: z.array(z.string()),license_or_access_notes: z.array(z.string()),conflicts: z.array(z.object({topic: z.enum(['dataset', 'metric']),paper_claim: z.string(),artifact_evidence: z.string(),severity: Severity,}),),})type DatasetArtifactReport = z.infer<typeof DatasetArtifactReport>const CitationFeedbackReport = z.object({source: z.literal('citation_feedback'),full_reproduction_found: z.boolean(),feedback_items: z.array(z.object({title: z.string(),url: z.string(),stance: z.string(),evidence: z.string(),signals: z.array(z.string()),}),),conflicts: z.array(z.object({topic: z.enum(['metric', 'result', 'dataset', 'code']),paper_claim: z.string(),artifact_evidence: z.string(),severity: Severity,}),),})type CitationFeedbackReport = z.infer<typeof CitationFeedbackReport>const ArtifactGapReport = z.object({source: z.literal('discovery_status'),found_artifacts: z.array(z.string()),missing_artifacts: z.array(z.string()),ambiguous_artifacts: z.array(z.string()),evidence_gaps: z.array(z.object({artifact: z.string(),why_it_matters: z.string(),severity: Severity,}),),})type ArtifactGapReport = z.infer<typeof ArtifactGapReport>const ReplicationPlanSchema = z.object({decision: z.enum(['reproduce', 'reproduce_with_caution', 'do_not_reproduce_yet']),risk_score: z.number().min(0).max(1),artifact_inventory: z.object({paper: z.object({title: z.string(),url: z.string(),venue_or_year: z.string(),}),code_repositories: z.array(z.object({repository: z.string(),url: z.string(),relationship: RepositoryRelationship,usability: z.string(),evidence: z.string(),next_action: z.string(),}),),datasets: z.array(z.object({name: z.string(),source_or_url: z.string(),access_status: DatasetAccessStatus,split_or_license_notes: z.string(),evidence: z.string(),next_action: z.string(),}),),reproduction_evidence: z.array(z.object({title: z.string(),url: z.string(),stance: z.string(),evidence: z.string(),next_action: z.string(),}),),}),evidence_conflicts: z.array(z.object({topic: EvidenceTopic,paper_claim: z.string(),external_evidence: z.string(),resolution: z.string(),severity: Severity,}),),missing_artifacts: z.array(z.string()),blocking_issues: z.array(z.string()),minimum_next_steps: z.array(z.string()),questions_for_authors: z.array(z.string()),confidence: z.number().min(0).max(1),})type ReplicationPlan = z.infer<typeof ReplicationPlanSchema>// ---------------------------------------------------------------------------// Agent configs// ---------------------------------------------------------------------------function sourceAgentPrompt(sourceName: string): string {return `You are the ${sourceName} for a paper replication triage workflow.Rules:- Use only the source snapshot in the task description.- Do not infer facts from general model knowledge.- Treat missing artifacts as evidence when the source says they are missing.- In live mode, never call evidence "seeded"; that word is reserved for mock fixtures.- Use an empty string for any URL field when the source does not provide a URL.- Return JSON only, matching the configured schema exactly.`}const paperClaimAgent: AgentConfig = {name: 'paper-claim-agent',model: PROVIDER.model,systemPrompt: `${sourceAgentPrompt('paper claim agent')}Extract the paper's own claims about code, data, metrics, reported results, and reproducibility promises.Prefer the paper_claims block when present; it is a deterministic extraction from Asta paper metadata and paper-scoped snippets.If paper_claims.snippet_scope says global_unscoped, treat unscoped_snippet_evidence only as uncertainty/discovery leads, not as target-paper claims.Do not add any field with status "unscoped_evidence" to claims; put it in uncertainties if relevant.If a paper_claims field says not_found_in_asta_sources, report it as an uncertainty instead of inventing a claim from general knowledge.Do not compare against external artifacts; that is the planner's job.The source field must be exactly "scholarly_metadata".Use topic "code" for code availability or compute/resource claims.`,maxTurns: 1,maxTokens: 900,temperature: 0.1,outputSchema: ClaimReport,}const codeArtifactAgent: AgentConfig = {name: 'code-artifact-agent',model: PROVIDER.model,systemPrompt: `${sourceAgentPrompt('code artifact agent')}Audit only code-discovery evidence. Focus on official repository status, missing files, training entrypoints, metric scripts, and GPU recipe clues.Do not treat GitHub search success as code availability by itself. Inspect README excerpts, repo relationship hints, relevant paths, linked-from-paper metadata, and issue evidence.Only mark a repository as likely_official when it is linked from paper metadata or the official-code claim is tied to the target paper; an "official implementation" claim for another method is not enough.Classify each candidate repository as likely_official, third_party, related_benchmark, unrelated, or unclear, and explain the evidence.For candidate_repositories, include repository URL, useful training/data/metric paths, and issue signals with URLs when available.The source field must be exactly "code_artifacts".Use topic "code" for official repo, missing file, or compute recipe conflicts. Use topic "metric" only for metric-script conflicts.`,maxTurns: 1,maxTokens: 1400,temperature: 0.1,outputSchema: CodeArtifactReport,}const datasetArtifactAgent: AgentConfig = {name: 'dataset-artifact-agent',model: PROVIDER.model,systemPrompt: `${sourceAgentPrompt('dataset artifact agent')}Audit only dataset evidence. Focus on split access, license restrictions, evaluator policy, and metric documentation.Live dataset evidence comes from Asta snippet_search plus dataset-related README/path clues from candidate repositories; this is not a general web crawl.If dataset_snippet_scope says global_unscoped, do not treat omitted/global Asta snippets as target-paper dataset facts.Prefer dataset_candidate_evidence when present: it contains generic, evidence-derived candidates with source_path, evidence_kind, evidence line, and confidence.Use target_tied_repository and repository_relationship_hints to separate target-paper dataset evidence from weak repository leads.Populate datasets only with concrete names supported by source evidence such as README lines, script names, data_path flags, paths, or snippets.Do not list CLI flags, column names, generic words, file names, or config keys as dataset names.If candidates are partial, report the confirmed subset and put the missing full roster/split protocol in conflicts or notes instead of guessing.Do not discuss code or citations unless the dataset source itself mentions them.The source field must be exactly "dataset_artifacts".`,maxTurns: 1,maxTokens: 1200,temperature: 0.1,outputSchema: DatasetArtifactReport,}const citationFeedbackAgent: AgentConfig = {name: 'citation-feedback-agent',model: PROVIDER.model,systemPrompt: `${sourceAgentPrompt('citation feedback agent')}Audit only follow-up citation/reproduction evidence. Separate full reproduction from partial support, metric mismatch, issue reports, and result gaps.Include URLs for citing papers, snippets, or reproduction discussions whenever the source provides them.If replication_snippet_scope says global_unscoped, do not create feedback_items from those snippets; treat the absence of paper-scoped feedback as an evidence gap.Do not treat ordinary citing papers as reproduction feedback unless their title, TLDR, abstract, or snippet explicitly mentions reproduction, replication, implementation, benchmark/result mismatch, metric protocol, code, data, or artifacts.Do not create feedback_items for the target paper itself, source_unavailable/tool-error records, or retrieval-status messages. Treat those as absence of evidence, not reproduction evidence.The source field must be exactly "citation_feedback".`,maxTurns: 1,maxTokens: 1100,temperature: 0.1,outputSchema: CitationFeedbackReport,}const artifactGapAgent: AgentConfig = {name: 'artifact-gap-agent',model: PROVIDER.model,systemPrompt: `${sourceAgentPrompt('artifact gap agent')}Summarize discovery status. Missing official code, missing data cards, unavailable citations, or ambiguous artifact ownership are not neutral; classify them as evidence gaps.Do not list non-blocking absence states as missing artifacts: no_restriction_signal means no private/restricted data signal was found, and not_required_for_replication means the optional artifact is not required unless another source explicitly says it is.Statuses found, no_restriction_signal, and not_required_for_replication belong in found_artifacts or notes, not missing_artifacts.The source field must be exactly "discovery_status".`,maxTurns: 1,maxTokens: 700,temperature: 0.1,outputSchema: ArtifactGapReport,}const replicationPlanner: AgentConfig = {name: 'replication-planner',model: PROVIDER.model,systemPrompt: `You are the final replication planner.You receive JSON reports from source-specific agents. Your job is evidence reconciliation, not generic paper summarization.Rules:1. Return JSON only, matching the ReplicationPlan schema.2. evidence_conflicts must list explicit paper claim vs external evidence conflicts.3. Allowed conflict topics only: code, dataset, metric, result, artifact_missing.4. Use topic "code" for compute recipe conflicts from repository evidence.5. In mock mode, preserve the seeded conflicts if the upstream reports include them.6. If official code or restricted data blocks reproduction, do not choose "reproduce" without caution.7. artifact_inventory must be useful to a researcher: name the paper, likely code repositories, datasets, and prior reproduction/citation evidence with URLs when available.8. minimum_next_steps should be concrete actions that do not require the user to already know the repo or dataset URL.9. artifact_inventory.reproduction_evidence must contain only external follow-up papers, reproduction attempts, or discussion threads. Do not include the target paper itself or tool failures there; put retrieval failures in missing_artifacts or blocking_issues.10. If a URL is unknown, use an empty string rather than inventing one.`,maxTurns: 2,maxTokens: 2000,temperature: 0.1,outputSchema: ReplicationPlanSchema,}// ---------------------------------------------------------------------------// Orchestrator// ---------------------------------------------------------------------------const progressTaskTitles = new Map<string, string>()function handleProgress(event: OrchestratorEvent): void {const dataTitle =event.data !== null&& typeof event.data === 'object'&& 'title' in event.data&& typeof (event.data as { title?: unknown }).title === 'string'? (event.data as { title: string }).title: undefinedif (event.task && dataTitle) {progressTaskTitles.set(event.task, dataTitle)}const taskTitle = dataTitle ?? (event.task ? progressTaskTitles.get(event.task) : undefined) ?? event.task ?? ''if (event.type === 'task_start') {console.log(` [START] ${taskTitle} -> ${event.agent ?? ''}`)}if (event.type === 'task_complete') {console.log(` [DONE] ${taskTitle}`)}}function buildOrchestrator(): OpenMultiAgent {return new OpenMultiAgent({defaultModel: PROVIDER.model,defaultProvider: PROVIDER.provider,...(PROVIDER.baseURL ? { defaultBaseURL: PROVIDER.baseURL } : {}),...(PROVIDER.apiKey ? { defaultApiKey: PROVIDER.apiKey } : {}),onProgress: handleProgress,})}function sourceTaskDescription(sourceLabel: string, snapshot: unknown): string {return `Audit this ${sourceLabel} source snapshot.Important:- This source is intentionally isolated from the other source snapshots.- Extract only what this source proves or fails to prove.- Return JSON only.SOURCE SNAPSHOT:${JSON.stringify(snapshot, null, 2)}`}function validateMockBundle(bundle: SourceBundle): void {if (bundle.mode !== 'mock') returnconst sourceText = JSON.stringify(bundle)const requiredSignals = ['Claimed official repository returns 404','benchmark_holdout is restricted and requires application approval','micro-F1','torchrun --nproc_per_node=4',]for (const signal of requiredSignals) {if (!sourceText.includes(signal)) {throw new Error(`Mock fixture validation failed: missing seeded signal "${signal}".`)}}}function assertPlannerDetectedConflicts(plan: ReplicationPlan, mode: SourceMode): void {if (mode !== 'mock') returnif (plan.evidence_conflicts.length < 2) {throw new Error(`Expected at least 2 seeded conflicts in mock mode, got ${plan.evidence_conflicts.length}. ` +'Tighten planner prompt or fixture evidence.',)}const topics = new Set(plan.evidence_conflicts.map((conflict) => conflict.topic))if (!topics.has('code') && !topics.has('dataset')) {throw new Error('Expected planner to detect at least one code or dataset conflict in mock mode.',)}}function verifySourceTasksStartedTogether(resultTasks: readonly TaskExecutionRecord[]): void {const sourceTitles = new Set(['Extract paper claims','Audit code artifacts','Audit dataset artifacts','Audit citation feedback','Audit artifact gaps',])const sourceTasks = resultTasks.filter((task) => sourceTitles.has(task.title))if (sourceTasks.length === sourceTitles.size&& sourceTasks.every((task) => task.metrics?.startMs !== undefined)) {const starts = sourceTasks.map((task) => task.metrics!.startMs)const startSkew = Math.max(...starts) - Math.min(...starts)console.log(`Source task start skew: ${startSkew}ms`)if (startSkew > 2000) {throw new Error(`Expected source audit tasks to start in parallel; observed ${startSkew}ms start skew.`,)}}}function printSourceDigest(digest: SourceDigest | undefined): void {if (!digest) returnconsole.log('LIVE SOURCE DISCOVERY SNAPSHOT')console.log('-'.repeat(78))console.log(`Paper: ${digest.paper.title}${digest.paper.venue_or_year ? ` (${digest.paper.venue_or_year})` : ''}`)if (digest.paper.url) {console.log(`Paper URL: ${digest.paper.url}`)}console.log('Candidate repositories:')if (digest.repositories.length === 0) {console.log(' - none discovered')}for (const repo of digest.repositories.slice(0, REPO_EVIDENCE_LIMIT)) {const hints = repo.relationship_hints.length > 0 ? `; hints: ${repo.relationship_hints.join(', ')}` : ''console.log(` - ${repo.repository}${repo.url ? ` (${repo.url})` : ''}${hints}`)if (repo.useful_paths.length > 0) {console.log(` paths: ${repo.useful_paths.slice(0, 4).join(', ')}`)}if (repo.issue_signals.length > 0) {console.log(` issues: ${repo.issue_signals.slice(0, 2).map((issue) => issue.title).join('; ')}`)}}console.log('Dataset clues from repositories:')if (digest.datasets.length === 0) {console.log(' - none discovered')} else {console.log(` - ${digest.datasets.slice(0, 12).map((dataset) => dataset.name).join(', ')}`)}console.log('Dataset mentions from paper snippets:')if (digest.dataset_snippet_mentions.length === 0) {console.log(' - none discovered')} else {console.log(` - ${digest.dataset_snippet_mentions.slice(0, 12).join(', ')}`)}console.log('Reproduction / follow-up signals:')if (digest.reproduction_signals.length === 0) {console.log(' - none discovered')}for (const signal of digest.reproduction_signals.slice(0, 5)) {console.log(` - ${signal.title}${signal.url ? ` (${signal.url})` : ''}`)}console.log()}// ---------------------------------------------------------------------------// Run// ---------------------------------------------------------------------------if (PROVIDER.missingEnv) {console.log(`[skip] LLM_PROVIDER=${PROVIDER.label} requires ${PROVIDER.missingEnv}.`)process.exit(0)}const bundle = await loadSourceBundle(PAPER_QUERY)validateMockBundle(bundle)const orchestrator = buildOrchestrator()const team = orchestrator.createTeam('paper-replication-triage-team', {name: 'paper-replication-triage-team',agents: [paperClaimAgent,codeArtifactAgent,datasetArtifactAgent,citationFeedbackAgent,artifactGapAgent,replicationPlanner,],sharedMemory: true,maxConcurrency: 5,})const tasks = [{title: 'Extract paper claims',description: sourceTaskDescription('scholarly metadata', bundle.scholarlyMetadata),assignee: 'paper-claim-agent',},{title: 'Audit code artifacts',description: sourceTaskDescription('code artifact', bundle.codeArtifacts),assignee: 'code-artifact-agent',},{title: 'Audit dataset artifacts',description: sourceTaskDescription('dataset artifact', bundle.datasetArtifacts),assignee: 'dataset-artifact-agent',},{title: 'Audit citation feedback',description: sourceTaskDescription('citation feedback', bundle.citationFeedback),assignee: 'citation-feedback-agent',},{title: 'Audit artifact gaps',description: sourceTaskDescription('discovery status', bundle.sourceIndex),assignee: 'artifact-gap-agent',},{title: 'Plan replication decision',description: `Reconcile the upstream source-specific reports for this paper query: "${bundle.query}".Use the prerequisite task outputs as the only evidence. Produce the final structured JSON report.`,assignee: 'replication-planner',dependsOn: ['Extract paper claims','Audit code artifacts','Audit dataset artifacts','Audit citation feedback','Audit artifact gaps',],},]console.log('Paper Replication Triage - Multi-Source Evidence Reconciliation')console.log('='.repeat(78))console.log(`Query: ${bundle.query}`)console.log(`Source mode: ${bundle.mode}`)console.log(`Provider: ${PROVIDER.label} (model=${PROVIDER.model})`)console.log('DAG: 5 source audits in parallel -> replication planner')console.log('='.repeat(78))console.log()printSourceDigest(bundle.sourceDigest)const result = await orchestrator.runTasks(team, tasks)if (result.tasks) {verifySourceTasksStartedTogether(result.tasks)}console.log('\n' + '='.repeat(78))console.log(`Overall success: ${result.success}`)console.log(`Tokens - input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)console.log()for (const [agentName, agentResult] of result.agentResults) {const status = agentResult.success ? 'OK ' : 'FAIL'const tokens = `in:${agentResult.tokenUsage.input_tokens} out:${agentResult.tokenUsage.output_tokens}`console.log(` [${status}] ${agentName.padEnd(24)} ${tokens}`)}const plannerResult = result.agentResults.get('replication-planner')if (!plannerResult?.success || !plannerResult.structured) {console.error('\nReplication planner failed or did not return structured output.')if (plannerResult?.output) {console.error(plannerResult.output)}process.exit(1)}const plan = plannerResult.structured as ReplicationPlanassertPlannerDetectedConflicts(plan, bundle.mode)console.log('\n' + '='.repeat(78))console.log('REPLICATION TRIAGE REPORT (JSON)')console.log('='.repeat(78))console.log(JSON.stringify(plan, null, 2))console.log('\nDone.')
open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。