@moda-ai/cli 1.12.2 → 1.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/harness-report.js +60 -3
- package/dist/harness.d.ts +6 -0
- package/dist/harness.js +203 -22
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
package/dist/harness-report.js
CHANGED
|
@@ -622,7 +622,8 @@ function graphEvidenceById(graph) {
|
|
|
622
622
|
const evidence = item.evidence
|
|
623
623
|
.filter((entry) => (isRecord(entry) &&
|
|
624
624
|
typeof entry.path === 'string' &&
|
|
625
|
-
typeof entry.reason === 'string'))
|
|
625
|
+
typeof entry.reason === 'string'))
|
|
626
|
+
.map((entry) => normalizeEvidenceEntry(entry));
|
|
626
627
|
if (evidence.length > 0)
|
|
627
628
|
byId.set(item.id, evidence);
|
|
628
629
|
}
|
|
@@ -637,10 +638,15 @@ function dedupeEvidence(evidence) {
|
|
|
637
638
|
if (seen.has(key))
|
|
638
639
|
continue;
|
|
639
640
|
seen.add(key);
|
|
640
|
-
deduped.push(
|
|
641
|
+
deduped.push(normalizeEvidenceEntry(item));
|
|
641
642
|
}
|
|
642
643
|
return deduped;
|
|
643
644
|
}
|
|
645
|
+
/** Keep the `{ path, reason }` shape, preserving a valid structured `line`. */
|
|
646
|
+
function normalizeEvidenceEntry(item) {
|
|
647
|
+
const line = evidenceLine(item);
|
|
648
|
+
return line ? { path: item.path, reason: item.reason, line } : { path: item.path, reason: item.reason };
|
|
649
|
+
}
|
|
644
650
|
function normalizeHarnessClaim(claim, graph) {
|
|
645
651
|
if (!isRecord(claim))
|
|
646
652
|
return claim;
|
|
@@ -792,11 +798,52 @@ function validateGraphCollection(graph, errors, field, shape) {
|
|
|
792
798
|
for (const [evidenceIndex, evidence] of item.evidence.entries()) {
|
|
793
799
|
if (!isRecord(evidence) || typeof evidence.path !== 'string' || typeof evidence.reason !== 'string') {
|
|
794
800
|
errors.push(`Graph ${label} evidence ${evidenceIndex + 1} must include path and reason.`);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (evidence.line !== undefined && (typeof evidence.line !== 'number' || !Number.isFinite(evidence.line) || evidence.line <= 0)) {
|
|
804
|
+
errors.push(`Graph ${label} evidence ${evidenceIndex + 1} line must be a positive number.`);
|
|
795
805
|
}
|
|
796
806
|
}
|
|
797
807
|
}
|
|
808
|
+
if (field === 'artifacts' && requiresArtifactEvidenceLine(item.type) && !artifactHasRetrievableLocation(item)) {
|
|
809
|
+
errors.push(`Graph ${label} is a ${String(item.type)} artifact and must include a numeric evidence line (or a dedicated whole-file source) so its exact text can be retrieved.`);
|
|
810
|
+
}
|
|
798
811
|
}
|
|
799
812
|
}
|
|
813
|
+
/** prompt/tool artifacts must pin an exact source location for downstream retrieval. */
|
|
814
|
+
function requiresArtifactEvidenceLine(type) {
|
|
815
|
+
return type === 'prompt' || type === 'tool';
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* A prompt/tool artifact's text is retrievable when either (a) at least one
|
|
819
|
+
* evidence entry carries a resolvable line, or (b) the artifact is a whole
|
|
820
|
+
* dedicated file — its evidence points at the same file named in `sourcePath`,
|
|
821
|
+
* so the entire file IS the artifact and no line is needed.
|
|
822
|
+
*/
|
|
823
|
+
function artifactHasRetrievableLocation(item) {
|
|
824
|
+
if (hasEvidenceLine(item.evidence))
|
|
825
|
+
return true;
|
|
826
|
+
return isWholeFileArtifact(item);
|
|
827
|
+
}
|
|
828
|
+
function isWholeFileArtifact(item) {
|
|
829
|
+
if (typeof item.sourcePath !== 'string' || item.sourcePath.length === 0)
|
|
830
|
+
return false;
|
|
831
|
+
if (!Array.isArray(item.evidence))
|
|
832
|
+
return false;
|
|
833
|
+
return item.evidence.some((entry) => isRecord(entry) && entry.path === item.sourcePath);
|
|
834
|
+
}
|
|
835
|
+
function hasEvidenceLine(evidence) {
|
|
836
|
+
if (!Array.isArray(evidence))
|
|
837
|
+
return false;
|
|
838
|
+
return evidence.some((entry) => (isRecord(entry) &&
|
|
839
|
+
typeof entry.path === 'string' &&
|
|
840
|
+
typeof entry.reason === 'string' &&
|
|
841
|
+
evidenceLine({
|
|
842
|
+
path: entry.path,
|
|
843
|
+
reason: entry.reason,
|
|
844
|
+
line: typeof entry.line === 'number' ? entry.line : undefined,
|
|
845
|
+
}) !== undefined));
|
|
846
|
+
}
|
|
800
847
|
function normalizeGraphItemFieldAliases(collection, item) {
|
|
801
848
|
if (!isRecord(item))
|
|
802
849
|
return;
|
|
@@ -1155,7 +1202,7 @@ function upsertCitation(citations, citationIndex, evidence, rootDir) {
|
|
|
1155
1202
|
return undefined;
|
|
1156
1203
|
if (evidence.path === '.')
|
|
1157
1204
|
return undefined;
|
|
1158
|
-
const line =
|
|
1205
|
+
const line = evidenceLine(evidence);
|
|
1159
1206
|
const key = `${evidence.path}:${line ?? ''}:${evidence.reason}`;
|
|
1160
1207
|
const existing = citationIndex.get(key);
|
|
1161
1208
|
if (existing)
|
|
@@ -1389,6 +1436,16 @@ function lineFromReason(reason) {
|
|
|
1389
1436
|
const value = Number(match[1]);
|
|
1390
1437
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
1391
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Resolve the 1-based source line for an evidence entry, preferring the
|
|
1441
|
+
* structured `line` field and falling back to a "line N" mention in the reason.
|
|
1442
|
+
*/
|
|
1443
|
+
function evidenceLine(evidence) {
|
|
1444
|
+
if (typeof evidence.line === 'number' && Number.isFinite(evidence.line) && evidence.line > 0) {
|
|
1445
|
+
return Math.trunc(evidence.line);
|
|
1446
|
+
}
|
|
1447
|
+
return lineFromReason(evidence.reason);
|
|
1448
|
+
}
|
|
1392
1449
|
function inferenceFromConfidence(confidence) {
|
|
1393
1450
|
if (confidence >= 0.85)
|
|
1394
1451
|
return 'direct';
|
package/dist/harness.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ declare const SCHEMA_VERSION = "harness.v0.1";
|
|
|
4
4
|
export interface HarnessEvidence {
|
|
5
5
|
path: string;
|
|
6
6
|
reason: string;
|
|
7
|
+
/**
|
|
8
|
+
* 1-based line where the cited item is defined. Optional in general, but
|
|
9
|
+
* required for `prompt` and `tool` artifacts so downstream prompt/tool
|
|
10
|
+
* optimization can retrieve the exact source text of every enumerated item.
|
|
11
|
+
*/
|
|
12
|
+
line?: number;
|
|
7
13
|
}
|
|
8
14
|
export interface HarnessDetection {
|
|
9
15
|
confidence: number;
|
package/dist/harness.js
CHANGED
|
@@ -1343,11 +1343,12 @@ function renderExternalHarnessAnalystPrompt(options) {
|
|
|
1343
1343
|
'',
|
|
1344
1344
|
'Your task:',
|
|
1345
1345
|
renderExternalAnalystProtocol(),
|
|
1346
|
-
'1. Identify the
|
|
1347
|
-
'2.
|
|
1348
|
-
|
|
1346
|
+
'1. Identify the runtime agent harnesses, agent families, channel adapters, identity mappings, model providers, frameworks, deployments, and open questions.',
|
|
1347
|
+
'2. Exhaustively enumerate EVERY prompt, tool, guardrail, and eval as its own graph artifact — the complete inventory, not a sample. This report feeds prompt optimization and tool optimization, so a missing tool or prompt is a real defect. Runtime agents may be modeled representatively on framework/example repos, but prompts and tools must be complete.',
|
|
1348
|
+
'3. Cite every narrative claim with local file paths, exact line numbers, and excerpts. File citations without line numbers are invalid for agent-authored reports. Enumerated artifacts only need evidence:[{path, reason}], so completeness of the artifact inventory is not limited by the citation cap.',
|
|
1349
|
+
`4. After completing investigation, print the report JSON between ${REPORT_JSON_BEGIN} and ${REPORT_JSON_END}. Moda CLI will validate it and write the local artifacts.`,
|
|
1349
1350
|
'Your final response must contain the sentinel-wrapped JSON report. Do not end with commentary about being read-only, do not summarize without JSON, and do not omit the sentinel markers.',
|
|
1350
|
-
'If the repository is
|
|
1351
|
+
'If the repository is very large, keep enumerating prompts/tools/guardrails/evals as artifacts (uncapped) and only summarize runtime AGENTS representatively — do not drop tools or prompts to save space.',
|
|
1351
1352
|
'',
|
|
1352
1353
|
'Final output format:',
|
|
1353
1354
|
REPORT_JSON_BEGIN,
|
|
@@ -1372,7 +1373,8 @@ function renderExternalHarnessAnalystPrompt(options) {
|
|
|
1372
1373
|
'Use exact field names and numeric confidence values from 0 to 1. Do not write string confidence values like "high". Do not use confidence >= 0.90 unless the claim is direct evidence from the cited line(s).',
|
|
1373
1374
|
'Each graph item with evidence must use evidence: [{ path, reason }], not citationIds/citations.',
|
|
1374
1375
|
'Runtime agents must include id, name, kind:"runtime_agent", root, entrypointIds, channelAdapterIds, artifactIds, identityNamespaceIds, deploymentIds, frameworkIds, modelProviderIds, confidence, evidence.',
|
|
1375
|
-
'Artifacts must include id, type, name, scope, usedByAgentIds, confidence, evidence, and optional sourcePath/contentHash/ownedByAgentId.',
|
|
1376
|
+
'Artifacts must include id, type, name, scope, usedByAgentIds, confidence, evidence, and optional sourcePath/contentHash/ownedByAgentId. Emit one artifact PER tool, PER prompt (including each inline system prompt), PER guardrail, and PER eval — enumerate them all; artifact types are entrypoint, prompt, tool, skill, eval, retrieval_index, memory, guardrail, model_config, unknown.',
|
|
1377
|
+
'For every `prompt` and `tool` artifact, at least one evidence entry MUST pin an exact source location so downstream optimization can retrieve the item text: carry a numeric `line` (the definition line, or the first line of the range) — evidence: [{ path, line, reason }]. The only exception is an artifact whose entire dedicated file is the text (e.g. a standalone prompt file); set its sourcePath and cite that same file and no line is required. When a single file holds multiple inline prompts or tool definitions, give each its own distinct line — a bare { path, reason } is a defect there.',
|
|
1376
1378
|
'Agent families must include id, name, runtimeAgentIds, sharedArtifactIds, confidence, evidence.',
|
|
1377
1379
|
'Frameworks/providers must include packageNames and runtimeAgentIds arrays; providers also include envVars and modelHints arrays.',
|
|
1378
1380
|
'Unknowns in graph must include id, question, candidateIds, evidence. Report-level unknowns must include id, question, citationIds.',
|
|
@@ -1393,11 +1395,15 @@ function renderExternalAnalystProtocol() {
|
|
|
1393
1395
|
return [
|
|
1394
1396
|
'Focused investigation protocol:',
|
|
1395
1397
|
'- First build a quick repo map with read-only commands such as pwd, git ls-files, rg, and targeted manifest/doc reads.',
|
|
1396
|
-
'-
|
|
1397
|
-
'-
|
|
1398
|
-
'-
|
|
1399
|
-
'-
|
|
1400
|
-
'-
|
|
1398
|
+
'- EXHAUSTIVELY enumerate every prompt, tool, guardrail, eval, memory/retrieval store, and model config in the repository — one graph artifact each. This inventory feeds downstream prompt optimization and tool optimization, so completeness matters more than brevity: do NOT stop at representative examples and do NOT sample.',
|
|
1399
|
+
'- Tools: emit a `tool` artifact for EVERY tool definition — each `@function_tool` / `function_tool(...)`-decorated function, every hosted/first-party tool class (e.g. WebSearchTool, FileSearchTool, ComputerTool, CodeInterpreterTool, HostedMCPTool, ImageGenerationTool, LocalShellTool), and every `tool({...})` / `createTool(...)` / `new *Tool(...)` factory. Name each artifact after the tool/function and cite its definition line.',
|
|
1400
|
+
'- Prompts: emit a `prompt` artifact for EVERY distinct prompt — dedicated prompt files AND inline system prompts (`instructions=`, `system=`, `SYSTEM_PROMPT`, prompt-template strings), one per agent/definition, each with its exact file+line location so the prompt text can be retrieved and optimized later.',
|
|
1401
|
+
'- Also emit artifacts for guardrails (type "guardrail"), evals/tests-of-agents (type "eval"), memory/session stores (type "memory"), retrieval indexes (type "retrieval_index"), and model configuration (type "model_config").',
|
|
1402
|
+
'- Only runtime AGENTS may be modeled representatively: if the repo is a framework/SDK/examples collection, you need not treat every example as a distinct production agent — but you MUST still enumerate every distinct tool, prompt, guardrail, and eval as its own artifact.',
|
|
1403
|
+
'- Each enumerated artifact needs evidence: [{ path, reason }] (file path + short reason); a full line+excerpt citation is required only for the narrative `claims`, NOT for every artifact — so listing hundreds of tools/prompts is cheap and expected.',
|
|
1404
|
+
'- EXCEPTION — `prompt` and `tool` artifacts MUST pin an exact location: put a numeric `line` on at least one evidence entry (evidence: [{ path, line, reason }]) at the definition line. That structured location is what lets downstream optimization retrieve the exact prompt/tool text; when one file has several inline prompts or tools, each artifact gets its own line. The only case that needs no line is a whole dedicated file whose entire content is the artifact (set sourcePath to that file and cite it). Still no excerpt required — just the line.',
|
|
1405
|
+
'- For every runtime agent, read and cite at least one concrete entrypoint or orchestration line, and link its prompts/tools via artifactIds.',
|
|
1406
|
+
'- Keep going until every prompt, tool, guardrail, and eval in scope is represented as an artifact. Use unknowns only for genuinely ambiguous items — never to skip enumeration. Respect the hard caps (<=250 claims, <=400 citations): artifacts are uncapped, so when there are many items keep enumerating them as artifacts and reserve claims/citations for the most important ones.',
|
|
1401
1407
|
].join('\n');
|
|
1402
1408
|
}
|
|
1403
1409
|
function renderAnalystPreflightSection(rootDir) {
|
|
@@ -2481,14 +2487,19 @@ function environmentFromAnnotation(annotation) {
|
|
|
2481
2487
|
};
|
|
2482
2488
|
}
|
|
2483
2489
|
function applyHeuristics(graph, files) {
|
|
2490
|
+
const isApplicationCode = applicationCodePredicate(files);
|
|
2484
2491
|
for (const file of files) {
|
|
2485
2492
|
maybeAddPromptArtifact(graph, file);
|
|
2486
2493
|
maybeAddSkillArtifact(graph, file);
|
|
2494
|
+
if (isApplicationCode(file.path))
|
|
2495
|
+
maybeAddToolArtifact(graph, file);
|
|
2487
2496
|
maybeAddEvalArtifact(graph, file);
|
|
2488
2497
|
maybeAddDeployment(graph, file);
|
|
2489
2498
|
}
|
|
2490
2499
|
if (graph.runtimeAgents.length === 0) {
|
|
2491
2500
|
for (const file of files) {
|
|
2501
|
+
if (!isApplicationCode(file.path))
|
|
2502
|
+
continue;
|
|
2492
2503
|
const agent = inferRuntimeAgent(file, graph);
|
|
2493
2504
|
if (agent)
|
|
2494
2505
|
upsertRuntimeAgent(graph, agent);
|
|
@@ -2587,7 +2598,10 @@ function inferRuntimeAgent(file, graph) {
|
|
|
2587
2598
|
const loweredText = file.text.toLowerCase();
|
|
2588
2599
|
const pathLooksLikeAgent = /(^|\/)(agent|agents|agent_harness)\//.test(loweredPath) ||
|
|
2589
2600
|
/(^|\/)(agent|main|index)\.(ts|tsx|js|mjs|py)$/.test(loweredPath);
|
|
2590
|
-
|
|
2601
|
+
// Require evidence that the file actually constructs or runs an agent, not
|
|
2602
|
+
// merely that it mentions a model provider. Loose signals like "openai" or
|
|
2603
|
+
// "model" match library internals and utilities that are not runtime agents.
|
|
2604
|
+
const codeLooksLikeAgent = AGENT_RUNTIME_SIGNAL.test(file.text);
|
|
2591
2605
|
if (!pathLooksLikeAgent || !codeLooksLikeAgent)
|
|
2592
2606
|
return null;
|
|
2593
2607
|
const channelIds = inferChannels(file);
|
|
@@ -2673,16 +2687,136 @@ function inferChannels(file) {
|
|
|
2673
2687
|
.filter(([needle]) => text.includes(needle))
|
|
2674
2688
|
.map(([, id]) => id);
|
|
2675
2689
|
}
|
|
2690
|
+
// Trailing module names an import statement references, normalized to their last
|
|
2691
|
+
// path segment without extension. Covers ES/TS `from '...'` / `import(...)` /
|
|
2692
|
+
// `require(...)` and Python `from a.b.c import ...` / `import a.b.c`. Used to link
|
|
2693
|
+
// split-file tools whose module is imported by extensionless or dotted name
|
|
2694
|
+
// (e.g. `from tools import get_weather`, `import { getWeather } from './tools'`).
|
|
2695
|
+
function importedModuleNames(text) {
|
|
2696
|
+
const names = new Set();
|
|
2697
|
+
for (const match of text.matchAll(/(?:from|import|require)\s*\(?\s*["']([^"']+)["']/g)) {
|
|
2698
|
+
const segment = match[1].split('/').pop();
|
|
2699
|
+
if (segment)
|
|
2700
|
+
names.add(segment.replace(/\.(ts|tsx|js|mjs|py)$/i, ''));
|
|
2701
|
+
}
|
|
2702
|
+
for (const match of text.matchAll(/^\s*(?:from|import)\s+([.\w]+)/gm)) {
|
|
2703
|
+
const segment = match[1].replace(/^\.+/, '').split('.').pop();
|
|
2704
|
+
if (segment)
|
|
2705
|
+
names.add(segment);
|
|
2706
|
+
}
|
|
2707
|
+
return names;
|
|
2708
|
+
}
|
|
2676
2709
|
function inferArtifactsForFile(file, graph) {
|
|
2677
2710
|
const text = file.text ?? '';
|
|
2711
|
+
const imported = importedModuleNames(text);
|
|
2678
2712
|
const referenced = graph.artifacts.filter((artifact) => {
|
|
2679
2713
|
if (!artifact.sourcePath)
|
|
2680
2714
|
return false;
|
|
2715
|
+
if (artifact.sourcePath === file.path)
|
|
2716
|
+
return true;
|
|
2681
2717
|
const base = basename(artifact.sourcePath);
|
|
2682
|
-
|
|
2718
|
+
if (text.includes(base) || text.includes(artifact.sourcePath))
|
|
2719
|
+
return true;
|
|
2720
|
+
// Split-file tools: the agent imports the tool module by its extensionless or
|
|
2721
|
+
// dotted name, which contains neither the basename-with-extension nor the full
|
|
2722
|
+
// source path (e.g. `from tools import get_weather`, `from './tools'`).
|
|
2723
|
+
if (artifact.type === 'tool') {
|
|
2724
|
+
const moduleName = base.replace(/\.(ts|tsx|js|mjs|py)$/i, '');
|
|
2725
|
+
if (moduleName && imported.has(moduleName))
|
|
2726
|
+
return true;
|
|
2727
|
+
}
|
|
2728
|
+
return false;
|
|
2683
2729
|
});
|
|
2684
2730
|
return referenced.map((artifact) => artifact.id);
|
|
2685
2731
|
}
|
|
2732
|
+
// Signals that a file constructs or runs an agent (as opposed to merely
|
|
2733
|
+
// importing a model SDK). Kept deliberately construction/run oriented so
|
|
2734
|
+
// library internals that only reference a provider are not misread as agents.
|
|
2735
|
+
// Covers the LangChain factory/executor shapes (create_tool_calling_agent,
|
|
2736
|
+
// create_openai_functions_agent, AgentExecutor, ...) and the `return`/`yield
|
|
2737
|
+
// Agent(...)` factory forms in addition to the direct construction/run signals.
|
|
2738
|
+
const AGENT_RUNTIME_SIGNAL = /(run_agent|runAgent|create_[a-z_]*agent\b|createAgent|initialize_agent|AgentExecutor\s*\(|new\s+Agent\s*\(|\bSwarm\s*\(|\bCrew\s*\(|\bRunner\s*\(|Runner\.run|\.run_sync\s*\(|\.run_streamed\s*\(|[=([,]\s*Agent\s*\(|(?:return|yield)\s+Agent\s*\(|^\s*Agent\s*\()/im;
|
|
2739
|
+
// Hosted/first-party tool classes that count as tool definitions when called.
|
|
2740
|
+
const HOSTED_TOOL_CLASSES = [
|
|
2741
|
+
'WebSearchTool',
|
|
2742
|
+
'FileSearchTool',
|
|
2743
|
+
'ComputerTool',
|
|
2744
|
+
'CodeInterpreterTool',
|
|
2745
|
+
'HostedMCPTool',
|
|
2746
|
+
'ImageGenerationTool',
|
|
2747
|
+
'LocalShellTool',
|
|
2748
|
+
'ShellTool',
|
|
2749
|
+
'ApplyPatchTool',
|
|
2750
|
+
'FunctionTool',
|
|
2751
|
+
];
|
|
2752
|
+
function repoOwnPackageNames(files) {
|
|
2753
|
+
const packageJson = files.find((file) => file.path === 'package.json')?.text;
|
|
2754
|
+
const pyproject = files.find((file) => file.path === 'pyproject.toml')?.text;
|
|
2755
|
+
const names = [
|
|
2756
|
+
packageJson ? safePackageName(packageJson) : undefined,
|
|
2757
|
+
pyproject ? safeTomlName(pyproject) : undefined,
|
|
2758
|
+
];
|
|
2759
|
+
return names.filter((name) => Boolean(name)).map((name) => name.toLowerCase());
|
|
2760
|
+
}
|
|
2761
|
+
function repoIsAgentFrameworkSource(files) {
|
|
2762
|
+
const own = new Set(repoOwnPackageNames(files));
|
|
2763
|
+
if (own.size === 0)
|
|
2764
|
+
return false;
|
|
2765
|
+
return FRAMEWORK_CATALOG.some((framework) => framework.packageNames.some((name) => own.has(name.toLowerCase())));
|
|
2766
|
+
}
|
|
2767
|
+
function isApplicationLikePath(path) {
|
|
2768
|
+
return /(^|\/)(examples?|apps?|demos?|samples?|cookbook|tutorials?|quickstart|playground)\//i.test(path);
|
|
2769
|
+
}
|
|
2770
|
+
// When the repository IS an agent framework/SDK (its own package name matches a
|
|
2771
|
+
// framework catalog entry), its library source is the framework implementation,
|
|
2772
|
+
// not customer application code: runtime agents and tools should only be
|
|
2773
|
+
// inferred from application-like directories (examples, apps, demos, ...).
|
|
2774
|
+
function applicationCodePredicate(files) {
|
|
2775
|
+
return repoIsAgentFrameworkSource(files) ? isApplicationLikePath : () => true;
|
|
2776
|
+
}
|
|
2777
|
+
const HOSTED_TOOL_CALL = new RegExp(`\\b(${HOSTED_TOOL_CLASSES.join('|')})\\s*\\(`, 'g');
|
|
2778
|
+
function detectToolDefinitions(file) {
|
|
2779
|
+
const text = file.text;
|
|
2780
|
+
if (!text)
|
|
2781
|
+
return [];
|
|
2782
|
+
const found = new Map();
|
|
2783
|
+
const pyDecorated = /@function_tool[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)/g;
|
|
2784
|
+
for (const match of text.matchAll(pyDecorated))
|
|
2785
|
+
found.set(match[1], '@function_tool-decorated function');
|
|
2786
|
+
const tsFactory = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:[\w.]*\.)?(?:tool|functionTool|createTool|dynamicTool)\s*\(/g;
|
|
2787
|
+
for (const match of text.matchAll(tsFactory))
|
|
2788
|
+
found.set(match[1], 'tool factory assignment');
|
|
2789
|
+
const tsNewTool = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(?:Dynamic|Structured)?Tool\b/g;
|
|
2790
|
+
for (const match of text.matchAll(tsNewTool))
|
|
2791
|
+
found.set(match[1], 'tool constructor assignment');
|
|
2792
|
+
for (const match of text.matchAll(HOSTED_TOOL_CALL))
|
|
2793
|
+
found.set(match[1], `hosted tool ${match[1]}`);
|
|
2794
|
+
return [...found.entries()].map(([name, reason]) => ({ name, reason }));
|
|
2795
|
+
}
|
|
2796
|
+
function maybeAddToolArtifact(graph, file) {
|
|
2797
|
+
if (!/\.(ts|tsx|js|mjs|py)$/i.test(file.path))
|
|
2798
|
+
return;
|
|
2799
|
+
// Skip test/eval fixtures — those tool definitions are fixtures, not a
|
|
2800
|
+
// customer's runtime tools. (Framework library internals are already
|
|
2801
|
+
// excluded by applyHeuristics's application-code predicate.)
|
|
2802
|
+
if (isEvalLikePath(file.path))
|
|
2803
|
+
return;
|
|
2804
|
+
const tools = detectToolDefinitions(file);
|
|
2805
|
+
if (tools.length === 0)
|
|
2806
|
+
return;
|
|
2807
|
+
for (const tool of tools) {
|
|
2808
|
+
upsertArtifact(graph, {
|
|
2809
|
+
id: stableId('tool', `${file.path}:${tool.name}`),
|
|
2810
|
+
type: 'tool',
|
|
2811
|
+
name: titleFromId(tool.name),
|
|
2812
|
+
scope: 'agent',
|
|
2813
|
+
sourcePath: file.path,
|
|
2814
|
+
usedByAgentIds: [],
|
|
2815
|
+
confidence: 0.6,
|
|
2816
|
+
evidence: [{ path: file.path, reason: `Tool definition detected (${tool.reason}).` }],
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2686
2820
|
function detectDeveloperAgentAdapters(files) {
|
|
2687
2821
|
const adapters = [];
|
|
2688
2822
|
if (files.some((file) => file.path.startsWith('.claude/'))) {
|
|
@@ -2745,7 +2879,7 @@ const FRAMEWORK_CATALOG = [
|
|
|
2745
2879
|
name: 'LangChain',
|
|
2746
2880
|
kind: 'agent_framework',
|
|
2747
2881
|
packageNames: ['langchain', '@langchain/core', '@langchain/langgraph', 'langgraph'],
|
|
2748
|
-
patterns: [/
|
|
2882
|
+
patterns: [/\blangchain\b/i, /\blanggraph\b/i],
|
|
2749
2883
|
},
|
|
2750
2884
|
{
|
|
2751
2885
|
id: 'framework_vercel_ai_sdk',
|
|
@@ -2759,7 +2893,7 @@ const FRAMEWORK_CATALOG = [
|
|
|
2759
2893
|
name: 'Mastra',
|
|
2760
2894
|
kind: 'agent_framework',
|
|
2761
2895
|
packageNames: ['mastra', '@mastra/core'],
|
|
2762
|
-
patterns: [/
|
|
2896
|
+
patterns: [/\bmastra\b/i],
|
|
2763
2897
|
},
|
|
2764
2898
|
{
|
|
2765
2899
|
id: 'framework_openai_agents',
|
|
@@ -2773,7 +2907,7 @@ const FRAMEWORK_CATALOG = [
|
|
|
2773
2907
|
name: 'CrewAI',
|
|
2774
2908
|
kind: 'agent_framework',
|
|
2775
2909
|
packageNames: ['crewai'],
|
|
2776
|
-
patterns: [/
|
|
2910
|
+
patterns: [/\bcrewai\b/i],
|
|
2777
2911
|
},
|
|
2778
2912
|
{
|
|
2779
2913
|
id: 'framework_pydantic_ai',
|
|
@@ -2798,7 +2932,7 @@ const PROVIDER_CATALOG = [
|
|
|
2798
2932
|
kind: 'model_provider',
|
|
2799
2933
|
packageNames: ['@anthropic-ai/sdk', '@ai-sdk/anthropic', 'anthropic'],
|
|
2800
2934
|
envVars: ['ANTHROPIC_API_KEY'],
|
|
2801
|
-
patterns: [/
|
|
2935
|
+
patterns: [/\banthropic\b/i, /\bclaude-[\w.-]+/i, /ANTHROPIC_API_KEY/],
|
|
2802
2936
|
},
|
|
2803
2937
|
{
|
|
2804
2938
|
id: 'provider_google',
|
|
@@ -2806,7 +2940,7 @@ const PROVIDER_CATALOG = [
|
|
|
2806
2940
|
kind: 'model_provider',
|
|
2807
2941
|
packageNames: ['@google/generative-ai', '@ai-sdk/google', 'google-generativeai'],
|
|
2808
2942
|
envVars: ['GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY'],
|
|
2809
|
-
patterns: [/
|
|
2943
|
+
patterns: [/\bgemini\b/i, /google\.generativeai/i],
|
|
2810
2944
|
},
|
|
2811
2945
|
{
|
|
2812
2946
|
id: 'provider_cohere',
|
|
@@ -2814,7 +2948,7 @@ const PROVIDER_CATALOG = [
|
|
|
2814
2948
|
kind: 'model_provider',
|
|
2815
2949
|
packageNames: ['cohere-ai', 'cohere'],
|
|
2816
2950
|
envVars: ['COHERE_API_KEY'],
|
|
2817
|
-
patterns: [/
|
|
2951
|
+
patterns: [/\bcohere\b/i],
|
|
2818
2952
|
},
|
|
2819
2953
|
{
|
|
2820
2954
|
id: 'provider_mistral',
|
|
@@ -2822,7 +2956,7 @@ const PROVIDER_CATALOG = [
|
|
|
2822
2956
|
kind: 'model_provider',
|
|
2823
2957
|
packageNames: ['@mistralai/mistralai', 'mistralai'],
|
|
2824
2958
|
envVars: ['MISTRAL_API_KEY'],
|
|
2825
|
-
patterns: [/
|
|
2959
|
+
patterns: [/\bmistral\b/i],
|
|
2826
2960
|
},
|
|
2827
2961
|
{
|
|
2828
2962
|
id: 'provider_ollama',
|
|
@@ -2830,7 +2964,7 @@ const PROVIDER_CATALOG = [
|
|
|
2830
2964
|
kind: 'model_provider',
|
|
2831
2965
|
packageNames: ['ollama'],
|
|
2832
2966
|
envVars: ['OLLAMA_HOST'],
|
|
2833
|
-
patterns: [/
|
|
2967
|
+
patterns: [/\bollama\b/i],
|
|
2834
2968
|
},
|
|
2835
2969
|
];
|
|
2836
2970
|
function detectAgentFrameworks(files) {
|
|
@@ -2883,9 +3017,9 @@ function detectionEvidence(files, packageNames, patterns) {
|
|
|
2883
3017
|
}
|
|
2884
3018
|
}
|
|
2885
3019
|
if (file.path === 'pyproject.toml' && file.text) {
|
|
2886
|
-
const
|
|
3020
|
+
const deps = pyprojectDependencyNames(file.text);
|
|
2887
3021
|
for (const name of packageNames) {
|
|
2888
|
-
if (
|
|
3022
|
+
if (deps.has(name.toLowerCase())) {
|
|
2889
3023
|
evidence.push({ path: file.path, reason: `Python dependency ${name} detected.` });
|
|
2890
3024
|
}
|
|
2891
3025
|
}
|
|
@@ -2915,6 +3049,53 @@ function packageDependencyNames(text) {
|
|
|
2915
3049
|
return new Set();
|
|
2916
3050
|
}
|
|
2917
3051
|
}
|
|
3052
|
+
function pyprojectDependencyNames(text) {
|
|
3053
|
+
// Only extract names from actual dependency contexts, never every quoted string
|
|
3054
|
+
// in the file: that would re-open the "ai"-inside-`keywords = ["ai"]` /
|
|
3055
|
+
// `name = "openai-agents"` false positives this parser exists to avoid. Two
|
|
3056
|
+
// dependency shapes are covered, and membership is tested by exact name:
|
|
3057
|
+
// - PEP 621 / PEP 518 requirement arrays (`dependencies`/`requires = [...]`,
|
|
3058
|
+
// `[project.optional-dependencies]`, `[dependency-groups]`): leading
|
|
3059
|
+
// distribution token of each quoted PEP 508 string (e.g. "openai>=2" -> openai).
|
|
3060
|
+
// - Poetry tables (`[tool.poetry.dependencies]`, `dev-dependencies`,
|
|
3061
|
+
// `group.<name>.dependencies`): the bare TOML key (e.g. `openai = "^1.0"`).
|
|
3062
|
+
const names = new Set();
|
|
3063
|
+
const addLeadingToken = (spec) => {
|
|
3064
|
+
const match = spec.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/);
|
|
3065
|
+
if (match)
|
|
3066
|
+
names.add(match[1].toLowerCase());
|
|
3067
|
+
};
|
|
3068
|
+
const isPoetryDepTable = (section) => /^tool\.poetry\.(dependencies|dev-dependencies|group\.[^.]+\.dependencies)$/.test(section);
|
|
3069
|
+
const isRequirementArrayTable = (section) => section === 'project.optional-dependencies' || section === 'dependency-groups';
|
|
3070
|
+
let section = '';
|
|
3071
|
+
let inDependencyArray = false;
|
|
3072
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
3073
|
+
const line = raw.split('#')[0];
|
|
3074
|
+
const header = line.match(/^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*$/);
|
|
3075
|
+
if (header) {
|
|
3076
|
+
section = header[1].trim().toLowerCase();
|
|
3077
|
+
inDependencyArray = false;
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
if (/^\s*(?:dependencies|requires)\s*=\s*\[/.test(line) ||
|
|
3081
|
+
(isRequirementArrayTable(section) && /=\s*\[/.test(line))) {
|
|
3082
|
+
inDependencyArray = true;
|
|
3083
|
+
}
|
|
3084
|
+
if (inDependencyArray) {
|
|
3085
|
+
for (const match of line.matchAll(/["']([^"']+)["']/g))
|
|
3086
|
+
addLeadingToken(match[1]);
|
|
3087
|
+
if (line.includes(']'))
|
|
3088
|
+
inDependencyArray = false;
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
if (isPoetryDepTable(section)) {
|
|
3092
|
+
const key = line.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/);
|
|
3093
|
+
if (key && key[1].toLowerCase() !== 'python')
|
|
3094
|
+
names.add(key[1].toLowerCase());
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
return names;
|
|
3098
|
+
}
|
|
2918
3099
|
function modelHintsForProvider(providerId, files) {
|
|
2919
3100
|
const hints = new Set();
|
|
2920
3101
|
const pattern = providerId === 'provider_anthropic'
|
package/package.json
CHANGED