← 全部示例
// 场景实例

论文复现分诊(多源证据调和)

并行审查不同证据来源,暴露分歧,并形成结构化的论文复现 go / no-go 计划。

适合需要判断论文是否值得、以及如何复现的研究团队

01 使用情境

论文论断、代码仓库和实际产物可能互不一致。只读论文,或让所有审查者使用已经混合的上下文,容易隐藏缺失证据与矛盾。

论文标题或 arXiv 标识符

默认使用明确标记的证据快照;live 模式可以接入 Asta 与 GitHub。

02 工作方式

一支分工明确、交接清楚的团队。

每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。

  1. 分别审查每个来源

    来源专属 Agent 并行检查不同证据快照。

    并行执行
    论文审查员

    提取论文论断与报告的实验设置。

    产物审查员

    检查可用实现与产物。

    证据审查员

    记录来源质量、缺口和冲突。

  2. 规划复现

    规划者通过依赖上下文接收结构化审查结果。

    复现规划者

    调和论文论断与产物,形成具体 go / no-go 计划。

03 最终结果

结构化复现分诊计划

计划明确展示不确定性和预置的来源冲突,不会给出虚假的整洁答案。

  1. 01逐来源证据审查
  2. 02缺失产物与矛盾
  3. 03复现决策与下一步计划
  • 证据来源可追溯。
  • 独立审查可以并行完成。
  • 规划者处理结构化分歧,而不是扁平摘要。
范围与限制
快照模式是确定性的演示。实时来源收集需要额外凭据与可选依赖,其证据质量仍需人工判断。
04 开发者实现

先运行,再查看具体实现。

上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。

实现信息 OpenMultiAgent
场景实例 2772 行

运行

在仓库的克隆里运行这个文件:

terminal
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"
前置条件
  • 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.
展开完整同步源码 · 2772 行

完整示例,从固定的 Framework commit 同步。

cookbook/paper-replication-triage.ts
/**
* 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_QUERY
const REPO_EVIDENCE_LIMIT = 3
const REPO_DISCOVERY_FETCH_LIMIT = 6
const SUPPORT_FILE_EXCERPT_LIMIT = 450
const README_EXCERPT_LIMIT = 1400
const ISSUE_BODY_EXCERPT_LIMIT = 360
const RELEVANT_PATH_LIMIT = 60
const ISSUE_EVIDENCE_LIMIT = 6
const DATASET_CANDIDATE_LIMIT_PER_REPO = 8
const DATASET_CANDIDATE_MIN_CONFIDENCE = 0.65
const DATASET_CONTEXT_LINE_LIMIT = 4
const DATASET_RELATED_PATH_LIMIT = 12
 
type 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: SourceMode
query: string
sourceIndex: unknown
scholarlyMetadata: unknown
codeArtifacts: unknown
datasetArtifacts: unknown
citationFeedback: unknown
sourceDigest?: SourceDigest
}
 
interface SourceDigest {
paper: {
title: string
url: string
venue_or_year: string
}
repositories: Array<{
repository: string
url: string
description: string
relationship_hints: string[]
useful_paths: string[]
issue_signals: Array<{
title: string
url: string
}>
}>
datasets: Array<{
name: string
evidence: string
source: string
}>
dataset_snippet_mentions: string[]
reproduction_signals: Array<{
title: string
url: string
source: string
}>
}
 
interface ProviderSettings {
label: ProviderChoice
provider: NonNullable<AgentConfig['provider']>
model: string
baseURL?: string
apiKey?: string
missingEnv?: string
}
 
type DatasetEvidenceKind =
| 'cli_arg'
| 'data_path'
| 'repository_path'
| 'readme_line'
| 'support_file_line'
 
interface DatasetCandidateEvidence {
name: string
evidence_kind: string
source_path: string
evidence: string
confidence: 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?: string
targetPaperMetadata?: unknown
metadataScope: PaperMetadataScope
note: string
}
 
type MCPClientConstructor = new (
info: { name: string; version: string },
options: { capabilities: Record<string, unknown> },
) => MCPClientLike
 
type StreamableHTTPClientTransportConstructor = new (
url: URL,
opts?: { requestInit?: RequestInit },
) => unknown
 
// ---------------------------------------------------------------------------
// Provider selection
// ---------------------------------------------------------------------------
 
function resolveProvider(): ProviderSettings {
const raw = (process.env.LLM_PROVIDER ?? 'anthropic').toLowerCase() as ProviderChoice
const modelOverride = process.env.LLM_MODEL
 
switch (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())) continue
seen.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 undefined
if (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 direct
 
for (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 undefined
if (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 direct
 
for (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) continue
if (stop.has(normalized.toLowerCase())) continue
terms.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) continue
queries.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 false
return normalizeTitleForCompare(text).includes(normalizedPhrase)
}
 
function textMentionsTarget(text: string, target: string): boolean {
if (textContainsNormalizedPhrase(text, target)) return true
 
const 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/gi
const matches = [...text.matchAll(nonOfficialPattern)]
if (matches.length === 0) return false
if (!target) return true
 
return matches.some((match) => {
const index = match.index ?? 0
return 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/gi
const matches = [...text.matchAll(officialPattern)]
if (matches.length === 0) return false
 
return matches.some((match) => {
const index = match.index ?? 0
const matchText = match[0] ?? ''
if (hasNegatedOfficialPrefix(text, index, matchText.length)) return false
if (!target) return true
return 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 0
 
let score = 0
if (/README/i.test(filePath)) score += 4
if (/(^|\/)scripts?\//i.test(filePath)) score += 5
if (/(^|\/)(configs?|experiments?)\//i.test(filePath)) score += 4
if (/(^|\/)(data|datasets?|download|prepare|preprocess)/i.test(filePath)) score += 4
if (/(train|training|eval|evaluation|metrics?|run|launch)/i.test(filePath)) score += 3
if (/\.(sh|py|yaml|yml|json|md)$/i.test(filePath)) score += 2
return 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) ?? raw
const 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 undefined
if (cleaned.includes('=')) return undefined
if (/^-/.test(cleaned)) return undefined
const lower = cleaned.toLowerCase()
if (DATASET_NAME_STOPWORDS.has(lower)) return undefined
if (/^(arg|args|cfg|config|param|kwargs?)[_.-]/i.test(cleaned)) return undefined
if (/(^|_)(path|size|len|length|step|id|type|flag|mode|root|features?)$/i.test(cleaned)) return undefined
if (/^\d+$/.test(cleaned)) return undefined
const 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 && !highPrecisionSource
const isLongAcronym = isAcronym && cleaned.length > 3
if (isAmbiguousShortAcronym) return undefined
if (looksLikeMethodOrVenue && !['cli_arg', 'data_path'].includes(evidenceKind)) return undefined
if (
!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.72
case 'data_path':
return 0.68
case 'repository_path':
return 0.58
case 'support_file_line':
return 0.54
case '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 undefined
return {
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) continue
const 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) ?? filePath
if (/\.(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 += 1
current.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 undefined
 
const 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)
: input
const 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'] === true
const 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'])) continue
for (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 : sourceQuery
const hasOfficialClaim = officialClaimNearTarget(readmeText, undefined)
const officialForTarget = targetQuery ? officialClaimNearTarget(readmeText, targetQuery) : hasOfficialClaim
const 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) continue
const 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 acc
if (value === null || typeof value !== 'object') return acc
if (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())) continue
collectRecordsWithTitle(child, acc)
}
return acc
}
 
function isUnavailableSourcePayload(value: unknown): boolean {
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
const 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 false
if (typeof value === 'string') return value.trim().length > 0
if (typeof value === 'boolean') return value
if (typeof value === 'number') return Number.isFinite(value)
if (Array.isArray(value)) return value.some(hasSuccessfulSourcePayload)
if (typeof value !== 'object') return false
if (isUnavailableSourcePayload(value)) return false
 
const 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 acc
if (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())) continue
collectSourceEvidenceStrings(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 = 0
if (!match || match.index === undefined) continue
 
const 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/i
const restrictionSignal = /\b(?:restricted|requires? approval|approval[- ]?gated|apply|application|private|hidden|permission|not public|unavailable|non-commercial|noncommercial|terms of use)\b/i
const 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/i
 
for (const segment of text.split(/\r?\n|(?<=[.!?])\s+/)) {
const normalized = compactText(segment, 320)
if (!normalized || paperLicenseNotice.test(normalized)) continue
if (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: unknown
claimSnippets: unknown
datasetSnippets: unknown
replicationSnippets: 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 = 0
if (repo['source_query'] === 'github-url-from-paper-metadata') score += 120
if (hints['linked_from_paper_metadata'] === true) score += 60
if (hints['mentions_query_or_title'] === true) score += 80
if (hints['mentions_official'] === true) score += 55
if (hints['official_claim_context_unclear'] === true) score -= 12
if (hints['explicit_non_official_claim'] === true) score -= 35
if (hints['mentions_reproduction'] === true) score += 20
if (hints['mentions_datasets'] === true) score += 15
 
if (paths.some((pathValue) => /(^|\/)(scripts?|experiments?)\//i.test(pathValue))) score += 25
if (paths.some((pathValue) => /(run|train|long|forecast|eval|metric).*\.(py|sh)$/i.test(pathValue))) score += 20
if (paths.some((pathValue) => /(^|\/)(data_provider|data_loader|datasets?)\//i.test(pathValue))) score += 15
 
const issueTitles = issues.map((issue) => String(issue['title'] ?? '')).join(' ')
if (/reproduc|replicat|result|hyperparameter|table/i.test(issueTitles)) score += 15
 
const 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') ?? 0
score += 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 scoreDelta
return 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
} = repo
return 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 true
 
const 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: unknown
citations: unknown
datasetSnippets: unknown
replicationSnippets: unknown
}
snippetScope: SnippetSearchScope
repoEvidence: Array<Record<string, unknown>>
datasetClues: Array<Record<string, unknown>>
directRepos: string[]
}): Array<Record<string, unknown>> {
const { asta, snippetScope, repoEvidence, datasetClues, directRepos } = input
const snippetsArePaperScoped = snippetScope === 'paper_scoped'
const scholarlyMetadataFound =
hasSuccessfulSourcePayload(asta.targetPaperMetadata) && findFirstStringByKey(asta.targetPaperMetadata, 'title') !== undefined
const 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 > 0
const scopedDatasetSnippets = snippetsArePaperScoped ? asta.datasetSnippets : undefined
const scopedReplicationSnippets = snippetsArePaperScoped ? asta.replicationSnippets : undefined
const 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 === 0
const targetTiedEntrypointRepos = targetTiedRepos.filter(repoHasTrainingOrEvalPath)
const untiedEntrypointReposFound =
targetTiedEntrypointRepos.length === 0 && repoEvidence.some(repoHasTrainingOrEvalPath)
const datasetEvidenceFound =
hasSuccessfulSourcePayload(scopedDatasetSnippets) || targetTiedDatasetClues.length > 0
const 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/i
const 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: unknown
citations: unknown
datasetSnippets: unknown
replicationSnippets: 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') ?? query
const 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)) continue
const 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)) continue
reproductionByKey.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)) continue
if (!hasReproductionFeedbackSignal(item)) continue
if (!title || reproductionByKey.has(url || title)) continue
reproductionByKey.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 false
if (candidate === target) return true
 
const targetTokens = significantTitleTokens(query)
if (targetTokens.length < 3) return false
 
const candidateTokens = significantTitleTokens(candidateTitle)
if (candidateTokens.length < 3) return false
 
const candidateTokenSet = new Set(candidateTokens)
const overlap = targetTokens.filter((token) => candidateTokenSet.has(token)).length
const targetCoverage = overlap / targetTokens.length
const candidateCoverage = overlap / candidateTokens.length
return 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)) continue
 
const 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 result
 
const 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: MCPClientConstructor
let StreamableHTTPClientTransport: StreamableHTTPClientTransportConstructor
try {
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.Client
StreamableHTTPClientTransport = 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.resolvedPaperId
const 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'])) continue
for (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 SnippetSearchScope
const 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'] : 0
return 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
: undefined
 
if (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') return
 
const 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') return
 
if (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) return
 
console.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 ReplicationPlan
assertPlannerDetectedConflicts(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.')
在 GitHub 查看 / 编辑
// 企业服务

要把它用到生产环境?

open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。

// 直接联系

把 Open Multi-Agent 用进真实业务

联系框架作者本人,帮你梳理 AI 落地目标、让 AI 真正与业务结合

可提供的工程服务
S-01

AI Agent 定制开发

业务梳理、Agent 设计、Prompt 评估、生产部署、私有化与持续支持。

S-02

多智能体系统集成

多 Agent 架构编排、RAG、CRM / ERP / API 对接、性能与稳定性调优。

S-03

企业 AI 咨询

AI 场景评估、技术选型、POC、ROI 估算与落地路线规划。